Merge #38309 from justinmk/doc2

This commit is contained in:
Justin M. Keyes
2026-03-16 10:57:15 -04:00
committed by GitHub
29 changed files with 513 additions and 412 deletions

View File

@@ -36,6 +36,8 @@ Reporting problems
Developer guidelines
--------------------
- New functionality should generally be implemented in Lua, not C. PRs [#37757](https://github.com/neovim/neovim/pull/37757), [#37831](https://github.com/neovim/neovim/pull/37831)
are excellent examples of this.
- Read [:help dev-quickstart](https://neovim.io/doc/user/dev_tools.html#dev-quickstart) to see how to run tests and start hacking on the codebase.
- Read [:help dev](https://neovim.io/doc/user/dev.html#dev) and [:help dev-doc][dev-doc-guide] if you are working on Nvim core.
- Read [:help dev-ui](https://neovim.io/doc/user/dev.html#dev-ui) if you are developing a UI.
@@ -195,7 +197,7 @@ Coding
You can run the linter locally by:
```bash
make lint
make lint # or lintc, lintlua, lintquery, lintdoc
```
### Style
@@ -206,7 +208,6 @@ make lint
```
This will format changed C, Lua, and treesitter query files with all
appropriate flags set.
- Style rules are (mostly) defined by `src/uncrustify.cfg` which tries to match
the [style-guide]. To use the Nvim `gq` command with `uncrustify`:
```vim
@@ -214,8 +215,8 @@ make lint
setlocal formatprg=uncrustify\ -q\ -l\ C\ -c\ src/uncrustify.cfg\ --no-backup
endif
```
- There is also `.clang-format` which has drifted from the [style-guide], but
is available for reference. To use the Nvim `gq` command with `clang-format`:
- There is also `.clang-format` which is "mostly" aligned with uncrustify.
To use the Nvim `gq` command with `clang-format`:
```vim
if !empty(findfile('.clang-format', ';'))
setlocal formatprg=clang-format\ -style=file

View File

@@ -261,7 +261,6 @@ Call |nvim_buf_attach()| to receive these events on the channel:
*nvim_buf_lines_event*
nvim_buf_lines_event[{buf}, {changedtick}, {firstline}, {lastline}, {linedata}, {more}]
When the buffer text between {firstline} and {lastline} (end-exclusive,
zero-indexed) were changed to the new text in the {linedata} list. The
granularity is a line, i.e. if a single character is changed in the
@@ -271,45 +270,41 @@ nvim_buf_lines_event[{buf}, {changedtick}, {firstline}, {lastline}, {linedata},
changed but not the buffer contents. {linedata} contains the changed
screen lines. This happens when 'inccommand' shows a buffer preview.
Properties: ~
{buf} API buffer handle (buffer number)
{changedtick} value of |b:changedtick| for the buffer. If you send an
API command back to nvim you can check the value of |b:changedtick| as
part of your request to ensure that no other changes have been made.
{firstline} integer line number of the first line that was replaced.
Zero-indexed: if line 1 was replaced then {firstline} will be zero,
not one. {firstline} is always less than or equal to the number of
lines that were in the buffer before the lines were replaced.
{lastline} integer line number of the first line that was not replaced
(i.e. the range {firstline}, {lastline} is end-exclusive).
Zero-indexed: if line numbers 2 to 5 were replaced, this will be 5
instead of 6. {lastline} is always be less than or equal to the number
of lines that were in the buffer before the lines were replaced.
{lastline} will be -1 if the event is part of the initial update after
attaching.
{linedata} list of strings containing the contents of the new buffer
lines. Newline characters are omitted; empty lines are sent as empty
strings.
{more} boolean, true for a "multipart" change notification: the
current change was chunked into multiple |nvim_buf_lines_event|
notifications (e.g. because it was too big).
Parameters: ~
{buf} (`integer`) Buffer id
• {changedtick} (`integer`) Value of |b:changedtick|. If you send an API
command back to Nvim you can check |b:changedtick| as
part of your request to ensure that no other changes
have been made.
• {firstline} (`integer`) The first line that was replaced.
Zero-indexed: if line 1 was replaced then {firstline}
will be zero, not one. Always less than or equal to
the number of lines that were in the buffer before the
lines were replaced.
• {lastline} (`integer`) The first line that was not replaced (i.e.
the range {firstline}, {lastline} is end-exclusive).
Zero-indexed: if line numbers 2 to 5 were replaced,
this will be 5 instead of 6. Always less than or equal
to the number of lines that were in the buffer before
the lines were replaced. Will be -1 if the event is
part of the initial update after attaching.
• {linedata} (`string[]`) Contents of the new buffer lines. Newline
characters are omitted; empty lines are sent as empty
strings.
• {more} (`boolean`) true for a "multipart" change notification:
the current change was chunked into multiple
|nvim_buf_lines_event| notifications (e.g. because it
was too big).
nvim_buf_changedtick_event[{buf}, {changedtick}] *nvim_buf_changedtick_event*
When |b:changedtick| was incremented but no text was changed. Relevant for
undo/redo.
Properties: ~
{buf} API buffer handle (buffer number)
{changedtick} new value of |b:changedtick| for the buffer
Parameters: ~
{buf} (`integer`) Buffer id
{changedtick} (`integer`) New value of |b:changedtick|.
nvim_buf_detach_event[{buf}] *nvim_buf_detach_event*
When buffer is detached (i.e. updates are disabled). Triggered explicitly by
|nvim_buf_detach()| or implicitly in these cases:
- Buffer was |abandon|ed and 'hidden' is not set.
@@ -317,8 +312,8 @@ nvim_buf_detach_event[{buf}] *nvim_buf_detach_event*
|:checktime| or 'autoread'.
- Generally: whenever the buffer contents are unloaded from memory.
Properties: ~
{buf} API buffer handle (buffer number)
Parameters: ~
{buf} (`integer`) Buffer id
EXAMPLE ~
@@ -409,7 +404,7 @@ after adding them, the returned |extmark| id can be used. >lua
See also |vim.hl.range()|.
==============================================================================
Floating windows *api-floatwin* *floating-windows*
Floating windows *api-floatwin* *floating-windows*
Floating windows ("floats") are displayed on top of normal windows. This is
useful to implement simple widgets, such as tooltips displayed next to the
@@ -3982,6 +3977,8 @@ nvim_win_set_config({window}, {config}) *nvim_win_set_config()*
Reconfigures the layout and properties of a window.
• Updates only the given keys; unspecified (`nil`) keys will not be
changed.
• Can move a window to another tabpage.
• Can transform a window to/from a float.
• Keys `row` / `col` / `relative` must be specified together.
• Cannot move the last window in a tabpage to a different one.
@@ -4191,8 +4188,8 @@ nvim_win_set_buf({window}, {buffer}) *nvim_win_set_buf()*
• {buffer} (`integer`) Buffer id
nvim_win_set_cursor({window}, {pos}) *nvim_win_set_cursor()*
Sets the (1,0)-indexed cursor position in the window. |api-indexing| This
scrolls the window even if it is not the current one.
Sets the (1,0)-indexed cursor position (byte offset) in the window.
|api-indexing| This scrolls the window even if it is not the current one.
Attributes: ~
Since: 0.1.0

View File

@@ -301,15 +301,15 @@ all loaded. For example, if this command: >
set runtimepath
produces this output:
produces this output: >
runtimepath=/etc/vim,~/.config/nvim,/usr/local/share/vim/vim82 ~
runtimepath=/etc/vim,~/.config/nvim,/usr/local/share/vim/vim82
then Vim will load all plugins in these directories and below:
then Vim will load all plugins in these directories and below: >
/etc/vim/plugin/ ~
~/.config/nvim/plugin/ ~
/usr/local/share/vim/vim82/plugin/ ~
/etc/vim/plugin/
~/.config/nvim/plugin/
/usr/local/share/vim/vim82/plugin/
Note that the last one is the value of $VIMRUNTIME which has been expanded.
@@ -323,9 +323,9 @@ happens when Vim starts up by using the |-V| argument: >
vim -V2
You will see a lot of messages, in between them is a remark about loading the
plugins. It starts with:
plugins. It starts with: >
Searching for "plugin/**/*.vim" in ~
Searching for "plugin/**/*.vim" in
There you can see where Vim looks for your plugin scripts.

View File

@@ -168,7 +168,7 @@ API
• |nvim_open_tabpage()| can open a new |tab-page|.
• |nvim_open_win()| floating windows can show a 'statusline'. Plugins can use
`style='minimal'` or `:setlocal statusline=` to hide the statusline.
• |nvim_win_set_config()| can move windows to other tab pages as floats.
• |nvim_win_set_config()| can move |floating-windows| to other tabpages.
• Added experimental |nvim__exec_lua_fast()| to allow remote API clients to
execute code while nvim is blocking for input.
• |vim.secure.trust()| accepts `path` for the `allow` action.

View File

@@ -6902,10 +6902,11 @@ A jump table for the options with a short description can be found at |Q_op|.
*'title'* *'notitle'*
'title' boolean (default off)
global
When on, the title of the window will be set to the value of
'titlestring' (if it is not empty), or to:
If enabled, Nvim will update the (GUI or terminal) window title. The
format is configured by 'titlestring'. By default it looks like: >
filename [+=-] (path) - Nvim
Where:
<
where: >
filename the name of the file being edited
- indicates the file cannot be modified, 'ma' off
+ indicates the file was modified
@@ -6913,6 +6914,7 @@ A jump table for the options with a short description can be found at |Q_op|.
=+ indicates the file is read-only and modified
(path) is the path of the file being edited
- Nvim the server name |v:servername| or "Nvim"
<
*'titlelen'*
'titlelen' number (default 85)
@@ -6938,32 +6940,32 @@ A jump table for the options with a short description can be found at |Q_op|.
*'titlestring'*
'titlestring' string (default "")
global
When this option is not empty, it will be used for the title of the
window. This happens only when the 'title' option is on.
Formats the window title, enabled by the 'title' option.
When this option contains printf-style '%' items, they will be
expanded according to the rules used for 'statusline'. If it contains
an invalid '%' format, the value is used as-is and no error or warning
will be given when the value is set.
Contains printf-style "%" items, expanded according to the rules of
'statusline'. If a "%" format is invalid, it is used as-is and no
error will be given.
The default behaviour is equivalent to: >vim
The default (empty) behaviour is equivalent to: >vim
set titlestring=%t%(\ %M%)%(\ \(%{expand(\"%:~:h\")}\)%)%a\ -\ Nvim
<
This option cannot be set in a modeline when 'modelineexpr' is off.
Example: >vim
auto BufEnter * let &titlestring = hostname() .. "/" .. expand("%:p")
set title titlestring=%<%F%=%l/%L-%P titlelen=70
< The value of 'titlelen' is used to align items in the middle or right
of the available space.
Some people prefer to have the file name first: >vim
< The value of 'titlelen' is used to align items in the middle
or right of the available space.
Example: to have the file name first: >vim
set titlestring=%t%(\ %M%)%(\ (%{expand(\"%:~:.:h\")})%)%(\ %a%)
< Note the use of "%{ }" and an expression to get the path of the file,
without the file name. The "%( %)" constructs are used to add a
< Note the use of "%{ }" and an expression to get the path of
the file, without the file name. The "%( %)" constructs add a
separating space only when needed.
NOTE: Use of special characters in 'titlestring' may cause the display
to be garbled (e.g., when it contains a CR or NL character).
This option cannot be set in a modeline when 'modelineexpr' is off.
*'ttimeout'* *'nottimeout'*
'ttimeout' boolean (default on)
global

View File

@@ -2507,6 +2507,8 @@ function vim.api.nvim_win_set_buf(window, buffer) end
--- Reconfigures the layout and properties of a window.
---
--- - Updates only the given keys; unspecified (`nil`) keys will not be changed.
--- - Can move a window to another tabpage.
--- - Can transform a window to/from a float.
--- - Keys `row` / `col` / `relative` must be specified together.
--- - Cannot move the last window in a tabpage to a different one.
---
@@ -2522,7 +2524,7 @@ function vim.api.nvim_win_set_buf(window, buffer) end
--- @param config vim.api.keyset.win_config Map defining the window configuration, see [nvim_open_win()]
function vim.api.nvim_win_set_config(window, config) end
--- Sets the (1,0)-indexed cursor position in the window. `api-indexing`
--- Sets the (1,0)-indexed cursor position (byte offset) in the window. `api-indexing`
--- This scrolls the window even if it is not the current one.
---
--- @param window integer `window-ID`, or 0 for current window

View File

@@ -7479,10 +7479,14 @@ vim.o.tm = vim.o.timeoutlen
vim.go.timeoutlen = vim.o.timeoutlen
vim.go.tm = vim.go.timeoutlen
--- When on, the title of the window will be set to the value of
--- 'titlestring' (if it is not empty), or to:
--- If enabled, Nvim will update the (GUI or terminal) window title. The
--- format is configured by 'titlestring'. By default it looks like:
--- ```
--- filename [+=-] (path) - Nvim
--- Where:
--- ```
---
--- where:
--- ```
--- filename the name of the file being edited
--- - indicates the file cannot be modified, 'ma' off
--- + indicates the file was modified
@@ -7490,6 +7494,8 @@ vim.go.tm = vim.go.timeoutlen
--- =+ indicates the file is read-only and modified
--- (path) is the path of the file being edited
--- - Nvim the server name `v:servername` or "Nvim"
--- ```
---
---
--- @type boolean
vim.o.title = false
@@ -7518,41 +7524,41 @@ vim.go.titlelen = vim.o.titlelen
vim.o.titleold = ""
vim.go.titleold = vim.o.titleold
--- When this option is not empty, it will be used for the title of the
--- window. This happens only when the 'title' option is on.
--- Formats the window title, enabled by the 'title' option.
---
--- When this option contains printf-style '%' items, they will be
--- expanded according to the rules used for 'statusline'. If it contains
--- an invalid '%' format, the value is used as-is and no error or warning
--- will be given when the value is set.
--- Contains printf-style "%" items, expanded according to the rules of
--- 'statusline'. If a "%" format is invalid, it is used as-is and no
--- error will be given.
---
--- The default behaviour is equivalent to:
--- The default (empty) behaviour is equivalent to:
---
--- ```vim
--- set titlestring=%t%(\ %M%)%(\ \(%{expand(\"%:~:h\")}\)%)%a\ -\ Nvim
--- ```
---
--- This option cannot be set in a modeline when 'modelineexpr' is off.
---
--- Example:
---
--- ```vim
--- auto BufEnter * let &titlestring = hostname() .. "/" .. expand("%:p")
--- set title titlestring=%<%F%=%l/%L-%P titlelen=70
--- ```
--- The value of 'titlelen' is used to align items in the middle or right
--- of the available space.
--- Some people prefer to have the file name first:
--- The value of 'titlelen' is used to align items in the middle
--- or right of the available space.
---
--- Example: to have the file name first:
---
--- ```vim
--- set titlestring=%t%(\ %M%)%(\ (%{expand(\"%:~:.:h\")})%)%(\ %a%)
--- ```
--- Note the use of "%{ }" and an expression to get the path of the file,
--- without the file name. The "%( %)" constructs are used to add a
--- Note the use of "%{ }" and an expression to get the path of
--- the file, without the file name. The "%( %)" constructs add a
--- separating space only when needed.
---
--- NOTE: Use of special characters in 'titlestring' may cause the display
--- to be garbled (e.g., when it contains a CR or NL character).
---
--- This option cannot be set in a modeline when 'modelineexpr' is off.
---
--- @type string
vim.o.titlestring = ""
vim.go.titlestring = vim.o.titlestring

View File

@@ -193,11 +193,10 @@ ArrayOf(DictAs(get_autocmds__ret)) nvim_get_autocmds(Dict(get_autocmds) *opts, A
buffers = arena_array(arena, 1);
ADD_C(buffers, STRING_OBJ(pat));
} else if (opts->buffer.type == kObjectTypeArray) {
if (opts->buffer.data.array.size > AUCMD_MAX_PATTERNS) {
api_set_error(err, kErrorTypeValidation, "Too many buffers (maximum of %d)",
AUCMD_MAX_PATTERNS);
VALIDATE((opts->buffer.data.array.size <= AUCMD_MAX_PATTERNS),
"Too many buffers (maximum of %d)", AUCMD_MAX_PATTERNS, {
goto cleanup;
}
});
buffers = arena_array(arena, kv_size(opts->buffer.data.array));
FOREACH_ITEM(opts->buffer.data.array, bufnr, {

View File

@@ -60,10 +60,9 @@ void nvim_ui_term_event(uint64_t channel_id, String event, Object value, Error *
FUNC_API_SINCE(12) FUNC_API_REMOTE_ONLY
{
if (strequal("termresponse", event.data)) {
if (value.type != kObjectTypeString) {
api_set_error(err, kErrorTypeValidation, "termresponse must be a string");
VALIDATE_T("termresponse", kObjectTypeString, value.type, {
return;
}
});
const String termresponse = value.data.string;
set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size);

View File

@@ -140,10 +140,9 @@ Object dict_get_value(dict_T *dict, String key, Arena *arena, Error *err)
{
dictitem_T *const di = tv_dict_find(dict, key.data, (ptrdiff_t)key.size);
if (di == NULL) {
api_set_error(err, kErrorTypeValidation, "Key not found: %s", key.data);
VALIDATE(di != NULL, "Key not found: %s", key.data, {
return (Object)OBJECT_INIT;
}
});
return vim_to_object(&di->di_tv, arena, true);
}
@@ -269,9 +268,9 @@ buf_T *find_buffer_by_handle(Buffer buffer, Error *err)
buf_T *rv = handle_get_buffer(buffer);
if (!rv) {
api_set_error(err, kErrorTypeValidation, "Invalid buffer id: %d", buffer);
}
VALIDATE_INT(rv, "buffer id", buffer, {
return NULL;
});
return rv;
}
@@ -284,9 +283,9 @@ win_T *find_window_by_handle(Window window, Error *err)
win_T *rv = handle_get_window(window);
if (!rv) {
api_set_error(err, kErrorTypeValidation, "Invalid window id: %d", window);
}
VALIDATE_INT(rv, "window id", window, {
return NULL;
});
return rv;
}
@@ -299,9 +298,9 @@ tabpage_T *find_tab_by_handle(Tabpage tabpage, Error *err)
tabpage_T *rv = handle_get_tabpage(tabpage);
if (!rv) {
api_set_error(err, kErrorTypeValidation, "Invalid tabpage id: %d", tabpage);
}
VALIDATE_INT(rv, "tabpage id", tabpage, {
return NULL;
});
return rv;
}
@@ -480,10 +479,9 @@ String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col
{
String rv = STRING_INIT;
if (lnum >= MAXLNUM) {
api_set_error(err, kErrorTypeValidation, "Line index is too high");
VALIDATE_RANGE((lnum < MAXLNUM), "line index", {
return rv;
}
});
char *bufstr = ml_get_buf(buf, (linenr_T)lnum);
colnr_T line_length = ml_get_buf_len(buf, (linenr_T)lnum);
@@ -720,7 +718,7 @@ bool api_object_to_bool(Object obj, const char *what, bool nil_value, Error *err
} else if (obj.type == kObjectTypeNil) {
return nil_value; // caller decides what NIL (missing retval in Lua) means
} else {
api_set_error(err, kErrorTypeValidation, "%s is not a boolean", what);
VALIDATE_EXP(false, what, "boolean", NULL, {});
return false;
}
}
@@ -734,7 +732,7 @@ int object_to_hl_id(Object obj, const char *what, Error *err)
int id = (int)obj.data.integer;
return (1 <= id && id <= highlight_num_groups()) ? id : 0;
} else {
api_set_error(err, kErrorTypeValidation, "Invalid hl_group: %s", what);
VALIDATE_S(false, "hl_group", what, {});
return 0;
}
}
@@ -1010,14 +1008,12 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err)
col = 0;
deleting = true;
} else {
if (col > MAXCOL) {
api_set_error(err, kErrorTypeValidation, "Column value outside range");
VALIDATE_RANGE(!(col > MAXCOL), "column", {
return res;
}
if (line < 1 || line > buf->b_ml.ml_line_count) {
api_set_error(err, kErrorTypeValidation, "Line value outside range");
});
VALIDATE_RANGE(!(line < 1 || line > buf->b_ml.ml_line_count), "line", {
return res;
}
});
}
assert(INT32_MIN <= line && line <= INT32_MAX);
pos_T pos = { (linenr_T)line, (int)col, 0 };

View File

@@ -57,6 +57,31 @@ void api_err_exp(Error *err, const char *name, const char *expected, const char
name, expected, actual);
}
/// Creates "Required: …" message and sets it on `err`.
void api_err_required(Error *err, const char *name)
{
ErrorType errtype = kErrorTypeValidation;
// Treat `name` without whitespace as a parameter (surround in quotes).
// Treat `name` with whitespace as a description (no quotes).
const char *has_space = strchr(name, ' ');
api_set_error(err, errtype, has_space ? "Required: %s" : "Required: '%s'", name);
}
/// Creates "Conflict: … not allowed with …" message and sets it on `err`.
void api_err_conflict(Error *err, const char *name, const char *name2)
{
ErrorType errtype = kErrorTypeValidation;
// Treat `name` without whitespace as a parameter (surround in quotes).
// Treat `name` with whitespace as a description (no quotes).
const char *has_space2 = strchr(name2, ' ');
api_set_error(err, errtype, has_space2
? "Conflict: '%s' not allowed with %s"
: "Conflict: '%s' not allowed with '%s'",
name, name2);
}
bool check_string_array(Array arr, char *name, bool disallow_nl, Error *err)
{
snprintf(IObuff, sizeof(IObuff), "'%s' item", name);

View File

@@ -16,6 +16,23 @@
} \
} while (0)
#define VALIDATE_R(cond, name, code) \
do { \
if (!(cond)) { \
api_err_required(err, name); \
code; \
} \
} while (0)
/// Shows a "not allowed with" message, for mutually-exclusive args.
#define VALIDATE_CON(cond, name, name2, code) \
do { \
if (!(cond)) { \
api_err_conflict(err, name, name2); \
code; \
} \
} while (0)
#define VALIDATE_INT(cond, name, val_, code) \
do { \
if (!(cond)) { \
@@ -88,7 +105,4 @@
} \
} while (0)
#define VALIDATE_R(cond, name, code) \
VALIDATE(cond, "Required: '%s'", name, code);
#include "api/private/validate.h.generated.h"

View File

@@ -447,7 +447,9 @@ static void ui_set_option(RemoteUI *ui, bool init, String name, Object value, Er
}
}
api_set_error(err, kErrorTypeValidation, "No such UI option: %s", name.data);
VALIDATE_S(false, "UI option", name.data, {
return;
});
}
/// Tell Nvim to resize a grid. Triggers a grid_resize event with the requested
@@ -495,8 +497,7 @@ void nvim_ui_pum_set_height(uint64_t channel_id, Integer height, Error *err)
}
if (!ui->ui_ext[kUIPopupmenu]) {
api_set_error(err, kErrorTypeValidation,
"It must support the ext_popupmenu option");
api_set_error(err, kErrorTypeValidation, "UI must support the ext_popupmenu option");
return;
}

View File

@@ -184,7 +184,7 @@ void nvim_set_hl(uint64_t channel_id, Integer ns_id, String name, Dict(highlight
// Setting URLs directly through highlight attributes is not supported
if (HAS_KEY(val, highlight, url)) {
api_set_error(err, kErrorTypeValidation, "Invalid Key: 'url'");
api_set_error(err, kErrorTypeValidation, "Invalid key: 'url'");
return;
}

View File

@@ -10,6 +10,7 @@
#include "nvim/api/private/converter.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/validate.h"
#include "nvim/api/vimscript.h"
#include "nvim/ascii_defs.h"
#include "nvim/buffer_defs.h"
@@ -303,8 +304,9 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Arena *arena,
object_to_vim(dict, &rettv, err);
break;
default:
api_set_error(err, kErrorTypeValidation, "dict argument type must be String or Dict");
return rv;
VALIDATE_EXP(false, "dict argument", "String or Dict", NULL, {
return rv;
});
}
dict_T *self_dict = rettv.vval.v_dict;
if (rettv.v_type != VAR_DICT || !self_dict) {
@@ -314,29 +316,26 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Arena *arena,
if (fn.data && fn.size > 0 && dict.type != kObjectTypeDict) {
dictitem_T *const di = tv_dict_find(self_dict, fn.data, (ptrdiff_t)fn.size);
if (di == NULL) {
api_set_error(err, kErrorTypeValidation, "Not found: %s", fn.data);
VALIDATE(di != NULL, "Not found: %s", fn.data, {
goto end;
}
});
if (di->di_tv.v_type == VAR_PARTIAL) {
api_set_error(err, kErrorTypeValidation,
"partial function not supported");
goto end;
}
if (di->di_tv.v_type != VAR_FUNC) {
api_set_error(err, kErrorTypeValidation, "Not a function: %s", fn.data);
VALIDATE((di->di_tv.v_type == VAR_FUNC), "Not a function: %s", fn.data, {
goto end;
}
});
fn = (String) {
.data = di->di_tv.vval.v_string,
.size = strlen(di->di_tv.vval.v_string),
};
}
if (!fn.data || fn.size < 1) {
api_set_error(err, kErrorTypeValidation, "Invalid (empty) function name");
VALIDATE((fn.data && fn.size >= 1), "Invalid function name: %s", "(empty)", {
goto end;
}
});
rv = _call_function(fn, args, self_dict, arena, err);
end:

View File

@@ -8,6 +8,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/dispatch.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/validate.h"
#include "nvim/api/win_config.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
@@ -738,6 +739,8 @@ restore_curwin:
/// Reconfigures the layout and properties of a window.
///
/// - Updates only the given keys; unspecified (`nil`) keys will not be changed.
/// - Can move a window to another tabpage.
/// - Can transform a window to/from a float.
/// - Keys `row` / `col` / `relative` must be specified together.
/// - Cannot move the last window in a tabpage to a different one.
///
@@ -1010,15 +1013,15 @@ static bool parse_float_bufpos(Array bufpos, lpos_T *out)
static void parse_bordertext(Object bordertext, BorderTextType bordertext_type, WinConfig *fconfig,
Error *err)
{
if (bordertext.type != kObjectTypeString && bordertext.type != kObjectTypeArray) {
api_set_error(err, kErrorTypeValidation, "title/footer must be string or array");
VALIDATE_EXP(!(bordertext.type != kObjectTypeString && bordertext.type != kObjectTypeArray),
"title/footer", "String or Array", api_typename(bordertext.type), {
return;
}
});
if (bordertext.type == kObjectTypeArray && bordertext.data.array.size == 0) {
api_set_error(err, kErrorTypeValidation, "title/footer cannot be an empty array");
VALIDATE_EXP(!(bordertext.type == kObjectTypeArray && bordertext.data.array.size == 0),
"title/footer", "non-empty Array", NULL, {
return;
}
});
bool *is_present;
VirtText *chunks;
@@ -1084,15 +1087,9 @@ static bool parse_bordertext_pos(win_T *wp, String bordertext_pos, BorderTextTyp
} else if (strequal(pos, "right")) {
*align = kAlignRight;
} else {
switch (bordertext_type) {
case kBorderTextTitle:
api_set_error(err, kErrorTypeValidation, "invalid title_pos value");
break;
case kBorderTextFooter:
api_set_error(err, kErrorTypeValidation, "invalid footer_pos value");
break;
}
return false;
VALIDATE_S(false, (bordertext_type == kBorderTextTitle ? "title_pos" : "footer_pos"), pos, {
return false;
});
}
return true;
}
@@ -1121,24 +1118,22 @@ void parse_border_style(Object style, WinConfig *fconfig, Error *err)
if (style.type == kObjectTypeArray) {
Array arr = style.data.array;
size_t size = arr.size;
if (!size || size > 8 || (size & (size - 1))) {
api_set_error(err, kErrorTypeValidation, "invalid number of border chars");
VALIDATE_EXP(!(!size || size > 8 || (size & (size - 1))),
"border", "1, 2, 4, or 8 chars", NULL, {
return;
}
});
for (size_t i = 0; i < size; i++) {
Object iytem = arr.items[i];
String string;
int hl_id = 0;
if (iytem.type == kObjectTypeArray) {
Array iarr = iytem.data.array;
if (!iarr.size || iarr.size > 2) {
api_set_error(err, kErrorTypeValidation, "invalid border char");
VALIDATE_EXP(!(!iarr.size || iarr.size > 2), "border", "1 or 2-item Array", NULL, {
return;
}
if (iarr.items[0].type != kObjectTypeString) {
api_set_error(err, kErrorTypeValidation, "invalid border char");
});
VALIDATE_EXP(iarr.items[0].type == kObjectTypeString, "border", "Array of Strings", NULL, {
return;
}
});
string = iarr.items[0].data.string;
if (iarr.size == 2) {
hl_id = object_to_hl_id(iarr.items[1], "border char highlight", err);
@@ -1149,13 +1144,14 @@ void parse_border_style(Object style, WinConfig *fconfig, Error *err)
} else if (iytem.type == kObjectTypeString) {
string = iytem.data.string;
} else {
api_set_error(err, kErrorTypeValidation, "invalid border char");
return;
VALIDATE_EXP(false, "border", "String or Array", api_typename(iytem.type), {
return;
});
}
if (string.size && mb_string2cells_len(string.data, string.size) > 1) {
api_set_error(err, kErrorTypeValidation, "border chars must be one cell");
VALIDATE_EXP(!(string.size && mb_string2cells_len(string.data, string.size) > 1),
"border", "only one-cell chars", NULL, {
return;
}
});
size_t len = MIN(string.size, sizeof(*chars) - 1);
if (len) {
memcpy(chars[i], string.data, len);
@@ -1168,12 +1164,13 @@ void parse_border_style(Object style, WinConfig *fconfig, Error *err)
memcpy(hl_ids + size, hl_ids, sizeof(*hl_ids) * size);
size <<= 1;
}
if ((chars[7][0] && chars[1][0] && !chars[0][0])
|| (chars[1][0] && chars[3][0] && !chars[2][0])
|| (chars[3][0] && chars[5][0] && !chars[4][0])
|| (chars[5][0] && chars[7][0] && !chars[6][0])) {
api_set_error(err, kErrorTypeValidation, "corner between used edges must be specified");
}
VALIDATE_EXP(!((chars[7][0] && chars[1][0] && !chars[0][0])
|| (chars[1][0] && chars[3][0] && !chars[2][0])
|| (chars[3][0] && chars[5][0] && !chars[4][0])
|| (chars[5][0] && chars[7][0] && !chars[6][0])), "border",
"corner char between edge chars", NULL, {
return;
});
} else if (style.type == kObjectTypeString) {
String str = style.data.string;
if (str.size == 0 || strequal(str.data, "none")) {
@@ -1199,7 +1196,9 @@ void parse_border_style(Object style, WinConfig *fconfig, Error *err)
return;
}
}
api_set_error(err, kErrorTypeValidation, "invalid border style \"%s\"", str.data);
VALIDATE_S(false, "border", str.data, {
return;
});
}
}
@@ -1207,10 +1206,10 @@ static void generate_api_error(win_T *wp, const char *attribute, Error *err)
{
if (wp != NULL && wp->w_floating) {
api_set_error(err, kErrorTypeValidation,
"Missing 'relative' field when reconfiguring floating window %d",
"Required: 'relative' when reconfiguring floating window %d",
wp->handle);
} else {
api_set_error(err, kErrorTypeValidation, "non-float cannot have '%s'", attribute);
VALIDATE_CON(false, attribute, "non-float window", {});
}
}
@@ -1265,16 +1264,15 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
{
bool has_relative = false, relative_is_win = false, is_split = false;
if (config->relative.size > 0) {
if (!parse_float_relative(config->relative, &fconfig->relative)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key");
VALIDATE_S(parse_float_relative(config->relative, &fconfig->relative),
"relative", config->relative.data, {
goto fail;
}
});
if (config->relative.size > 0 && !(HAS_KEY_X(config, row) && HAS_KEY_X(config, col))
&& !HAS_KEY_X(config, bufpos)) {
api_set_error(err, kErrorTypeValidation, "'relative' requires 'row'/'col' or 'bufpos'");
VALIDATE_R(!(config->relative.size > 0 && !(HAS_KEY_X(config, row) && HAS_KEY_X(config, col))
&& !HAS_KEY_X(config, bufpos)), "'relative' requires 'row'/'col' or 'bufpos'", {
goto fail;
}
});
has_relative = true;
fconfig->external = false;
@@ -1287,35 +1285,34 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
is_split = true;
fconfig->external = false;
} else if (wp == NULL) { // new win
api_set_error(err, kErrorTypeValidation,
"Must specify 'relative' or 'external' when creating a float");
goto fail;
VALIDATE_R(false, "'relative' or 'external' when creating a float", {
goto fail;
});
}
}
if (HAS_KEY_X(config, vertical)) {
if (!is_split) {
api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'vertical'");
goto fail;
}
}
VALIDATE_CON(!(HAS_KEY_X(config, vertical) && !is_split), "vertical", "floating windows", {
goto fail;
});
VALIDATE_CON(!(HAS_KEY_X(config, split) && !is_split), "split", "floating windows", {
goto fail;
});
if (HAS_KEY_X(config, split)) {
if (!is_split) {
api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'split'");
VALIDATE_CON(is_split, "split", "floating windows", {
goto fail;
}
if (!parse_config_split(config->split, &fconfig->split)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'split' key");
});
VALIDATE_S(parse_config_split(config->split, &fconfig->split), "split", config->split.data, {
goto fail;
}
});
}
if (HAS_KEY_X(config, anchor)) {
if (!parse_float_anchor(config->anchor, &fconfig->anchor)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'anchor' key");
VALIDATE_S(parse_float_anchor(config->anchor, &fconfig->anchor),
"anchor", config->anchor.data, {
goto fail;
}
});
}
if (HAS_KEY_X(config, row)) {
@@ -1339,10 +1336,10 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
generate_api_error(wp, "bufpos", err);
goto fail;
} else {
if (!parse_float_bufpos(config->bufpos, &fconfig->bufpos)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'bufpos' key");
VALIDATE_EXP(parse_float_bufpos(config->bufpos, &fconfig->bufpos),
"bufpos", "[row, col] array", NULL, {
goto fail;
}
});
if (!HAS_KEY_X(config, row)) {
fconfig->row = (fconfig->anchor & kFloatAnchorSouth) ? 0 : 1;
@@ -1354,46 +1351,42 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
}
if (HAS_KEY_X(config, width)) {
if (config->width > 0) {
fconfig->width = (int)config->width;
} else {
api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer");
VALIDATE_EXP((config->width > 0), "width", "positive Integer", NULL, {
goto fail;
}
});
fconfig->width = (int)config->width;
} else if (!reconf && !is_split) {
api_set_error(err, kErrorTypeValidation, "Must specify 'width'");
goto fail;
VALIDATE_R(false, "width", {
goto fail;
});
}
if (HAS_KEY_X(config, height)) {
if (config->height > 0) {
fconfig->height = (int)config->height;
} else {
api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer");
VALIDATE_EXP((config->height > 0), "height", "positive Integer", NULL, {
goto fail;
}
});
fconfig->height = (int)config->height;
} else if (!reconf && !is_split) {
api_set_error(err, kErrorTypeValidation, "Must specify 'height'");
goto fail;
VALIDATE_R(false, "height", {
goto fail;
});
}
if (HAS_KEY_X(config, external)) {
fconfig->external = config->external;
if (has_relative && fconfig->external) {
api_set_error(err, kErrorTypeValidation,
"Only one of 'relative' and 'external' must be used");
VALIDATE_CON(!(has_relative && fconfig->external), "relative", "external", {
goto fail;
}
});
if (fconfig->external && !ui_has(kUIMultigrid)) {
api_set_error(err, kErrorTypeValidation, "UI doesn't support external windows");
goto fail;
}
}
if (HAS_KEY_X(config, win) && fconfig->external) {
api_set_error(err, kErrorTypeValidation, "external window cannot have 'win'");
VALIDATE_CON(!(HAS_KEY_X(config, win) && fconfig->external), "win", "external window", {
goto fail;
}
});
if (relative_is_win || (HAS_KEY_X(config, win) && !is_split && wp && wp->w_floating
&& fconfig->relative == kFloatRelativeWindow)) {
// When relative=win is given, missing win field means win=0.
@@ -1409,11 +1402,11 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
} else {
// Handle is not validated here, as win_config_split can accept negative values.
if (HAS_KEY_X(config, win)) {
if (!is_split && !has_relative && (!wp || !wp->w_floating)) {
api_set_error(err, kErrorTypeValidation,
"non-float with 'win' requires at least 'split' or 'vertical'");
VALIDATE_R(!(!is_split && !has_relative && (!wp || !wp->w_floating)),
"non-float with 'win' requires 'split' or 'vertical'", {
goto fail;
}
});
fconfig->window = config->win;
}
// Resolve, but skip validating. E.g: win_config_split accepts negative "win".
@@ -1432,23 +1425,19 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
}
if (HAS_KEY_X(config, zindex)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'zindex'");
VALIDATE_CON(!is_split, "zindex", "non-float window", {
goto fail;
}
if (config->zindex > 0) {
fconfig->zindex = (int)config->zindex;
} else {
api_set_error(err, kErrorTypeValidation, "'zindex' key must be a positive Integer");
});
VALIDATE_EXP((config->zindex > 0), "zindex", "positive Integer", NULL, {
goto fail;
}
});
fconfig->zindex = (int)config->zindex;
}
if (HAS_KEY_X(config, title)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'title'");
VALIDATE_CON(!is_split, "title", "non-float window", {
goto fail;
}
});
parse_bordertext(config->title, kBorderTextTitle, fconfig, err);
if (ERROR_SET(err)) {
@@ -1460,17 +1449,15 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
goto fail;
}
} else {
if (HAS_KEY_X(config, title_pos)) {
api_set_error(err, kErrorTypeException, "title_pos requires title to be set");
VALIDATE_R(!HAS_KEY_X(config, title_pos), "'title' requires 'title_pos'", {
goto fail;
}
});
}
if (HAS_KEY_X(config, footer)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'footer'");
VALIDATE_CON(!is_split, "footer", "non-float window", {
goto fail;
}
});
parse_bordertext(config->footer, kBorderTextFooter, fconfig, err);
if (ERROR_SET(err)) {
@@ -1482,18 +1469,16 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
goto fail;
}
} else {
if (HAS_KEY_X(config, footer_pos)) {
api_set_error(err, kErrorTypeException, "footer_pos requires footer to be set");
VALIDATE_R(!HAS_KEY_X(config, footer_pos), "'footer' requires 'footer_pos'", {
goto fail;
}
});
}
Object border_style = OBJECT_INIT;
if (HAS_KEY_X(config, border)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'");
VALIDATE_CON(!is_split, "border", "non-float window", {
goto fail;
}
});
border_style = config->border;
if (border_style.type != kObjectTypeNil) {
parse_border_style(border_style, fconfig, err);
@@ -1512,15 +1497,15 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
} else if (striequal(config->style.data, "minimal")) {
fconfig->style = kWinStyleMinimal;
} else {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'style' key");
goto fail;
VALIDATE_S(false, "style", config->style.data, {
goto fail;
});
}
}
if (HAS_KEY_X(config, noautocmd)) {
if (wp && config->noautocmd != fconfig->noautocmd) {
api_set_error(err, kErrorTypeValidation,
"'noautocmd' cannot be changed with existing windows");
api_set_error(err, kErrorTypeValidation, "'noautocmd' cannot be changed on existing window");
goto fail;
}
fconfig->noautocmd = config->noautocmd;

View File

@@ -92,7 +92,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Arena *arena, Error *err)
return rv;
}
/// Sets the (1,0)-indexed cursor position in the window. |api-indexing|
/// Sets the (1,0)-indexed cursor position (byte offset) in the window. |api-indexing|
/// This scrolls the window even if it is not the current one.
///
/// @param window |window-ID|, or 0 for current window
@@ -107,26 +107,21 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err)
return;
}
if (pos.size != 2 || pos.items[0].type != kObjectTypeInteger
|| pos.items[1].type != kObjectTypeInteger) {
api_set_error(err,
kErrorTypeValidation,
"Argument \"pos\" must be a [row, col] array");
VALIDATE_EXP(!(pos.size != 2 || pos.items[0].type != kObjectTypeInteger
|| pos.items[1].type != kObjectTypeInteger), "pos", "[row, col] array", NULL, {
return;
}
});
int64_t row = pos.items[0].data.integer;
int64_t col = pos.items[1].data.integer;
if (row <= 0 || row > win->w_buffer->b_ml.ml_line_count) {
api_set_error(err, kErrorTypeValidation, "Cursor position outside buffer");
VALIDATE_RANGE(!(row <= 0 || row > win->w_buffer->b_ml.ml_line_count), "cursor line", {
return;
}
});
if (col > MAXCOL || col < 0) {
api_set_error(err, kErrorTypeValidation, "Column value outside range");
VALIDATE_RANGE(!(col > MAXCOL || col < 0), "cursor column", {
return;
}
});
win->w_cursor.lnum = (linenr_T)row;
win->w_cursor.col = (colnr_T)col;
@@ -450,9 +445,9 @@ void nvim_win_set_hl_ns(Window window, Integer ns_id, Error *err)
}
// -1 is allowed as inherit global namespace
if (ns_id < -1) {
api_set_error(err, kErrorTypeValidation, "no such namespace");
}
VALIDATE_S((ns_id >= -1), "namespace", "", {
return;
});
win->w_ns_hl = (NS)ns_id;
win->w_hl_needs_update = true;

View File

@@ -9677,10 +9677,11 @@ local options = {
cb = 'did_set_title_icon',
defaults = false,
desc = [=[
When on, the title of the window will be set to the value of
'titlestring' (if it is not empty), or to:
If enabled, Nvim will update the (GUI or terminal) window title. The
format is configured by 'titlestring'. By default it looks like: >
filename [+=-] (path) - Nvim
Where:
<
where: >
filename the name of the file being edited
- indicates the file cannot be modified, 'ma' off
+ indicates the file was modified
@@ -9688,6 +9689,7 @@ local options = {
=+ indicates the file is read-only and modified
(path) is the path of the file being edited
- Nvim the server name |v:servername| or "Nvim"
<
]=],
full_name = 'title',
scope = { 'global' },
@@ -9735,31 +9737,31 @@ local options = {
cb = 'did_set_titlestring',
defaults = '',
desc = [=[
When this option is not empty, it will be used for the title of the
window. This happens only when the 'title' option is on.
Formats the window title, enabled by the 'title' option.
When this option contains printf-style '%' items, they will be
expanded according to the rules used for 'statusline'. If it contains
an invalid '%' format, the value is used as-is and no error or warning
will be given when the value is set.
Contains printf-style "%" items, expanded according to the rules of
'statusline'. If a "%" format is invalid, it is used as-is and no
error will be given.
The default behaviour is equivalent to: >vim
The default (empty) behaviour is equivalent to: >vim
set titlestring=%t%(\ %M%)%(\ \(%{expand(\"%:~:h\")}\)%)%a\ -\ Nvim
<
This option cannot be set in a modeline when 'modelineexpr' is off.
Example: >vim
auto BufEnter * let &titlestring = hostname() .. "/" .. expand("%:p")
set title titlestring=%<%F%=%l/%L-%P titlelen=70
< The value of 'titlelen' is used to align items in the middle or right
of the available space.
Some people prefer to have the file name first: >vim
< The value of 'titlelen' is used to align items in the middle
or right of the available space.
Example: to have the file name first: >vim
set titlestring=%t%(\ %M%)%(\ (%{expand(\"%:~:.:h\")})%)%(\ %a%)
< Note the use of "%{ }" and an expression to get the path of the file,
without the file name. The "%( %)" constructs are used to add a
< Note the use of "%{ }" and an expression to get the path of
the file, without the file name. The "%( %)" constructs add a
separating space only when needed.
NOTE: Use of special characters in 'titlestring' may cause the display
to be garbled (e.g., when it contains a CR or NL character).
This option cannot be set in a modeline when 'modelineexpr' is off.
]=],
full_name = 'titlestring',
modelineexpr = true,

View File

@@ -119,6 +119,11 @@ end
describe('API: buffer events:', function()
before_each(clear)
it('validation', function()
local b = editoriginal(false)
eq("Invalid key: 'builtin'", pcall_err(api.nvim_buf_attach, b, false, { builtin = 'asfd' }))
end)
it('when lines are added', function()
local b, tick = editoriginal(true)
@@ -798,11 +803,6 @@ describe('API: buffer events:', function()
expectn('nvim_buf_changedtick_event', { b, tick })
end)
it('returns a proper error on nonempty options dict', function()
local b = editoriginal(false)
eq("Invalid key: 'builtin'", pcall_err(api.nvim_buf_attach, b, false, { builtin = 'asfd' }))
end)
it('nvim_buf_attach returns response after delay #8634', function()
sleep(250)
-- response

View File

@@ -143,7 +143,7 @@ describe('API/extmarks', function()
)
-- No memory leak with virt_text, virt_lines, sign_text
eq(
'right_gravity is not a boolean',
"Invalid 'right_gravity': expected boolean",
pcall_err(set_extmark, ns, marks[2], 0, 0, {
virt_text = { { 'foo', 'Normal' } },
virt_lines = { { { 'bar', 'Normal' } } },

View File

@@ -116,6 +116,9 @@ describe('API: set highlight', function()
"Invalid 'blend': expected Integer, got Array",
pcall_err(api.nvim_set_hl, 0, 'Test_hl3', { fg = '#FF00FF', blend = {} })
)
-- 'url' is rejected. #38162
eq("Invalid key: 'url'", pcall_err(api.nvim_set_hl, 0, 'Test', { url = 'https://example.com' }))
assert_alive()
end)
it('can set gui highlight', function()
@@ -254,10 +257,6 @@ describe('API: set highlight', function()
)
assert_alive()
end)
it("'url' is rejected with an error #38162", function()
eq("Invalid Key: 'url'", pcall_err(api.nvim_set_hl, 0, 'Test', { url = 'https://example.com' }))
assert_alive()
end)
end)
describe('API: get highlight', function()

View File

@@ -649,7 +649,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
eq('Invalid mode shortname: "xnoremap"', pcall_err(api.nvim_del_keymap, 'xnoremap', 'lhs'))
end)
it('error on invalid optnames', function()
it('validation', function()
eq(
"Invalid key: 'silentt'",
pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { silentt = true })
@@ -659,16 +659,13 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
"Invalid key: 'nowaiT'",
pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { nowaiT = false })
)
end)
it('error on <buffer> option key', function()
-- <buffer> option key
eq(
"Invalid key: 'buffer'",
pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { buffer = true })
)
end)
it('error when "replace_keycodes" is used without "expr"', function()
eq(
'"replace_keycodes" requires "expr"',
pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { replace_keycodes = true })
@@ -681,7 +678,10 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
it('throws an error when given non-boolean value for ' .. opt, function()
local opts = {}
opts[opt] = 'fooo'
eq(opt .. ' is not a boolean', pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', opts))
eq(
("Invalid '%s': expected boolean"):format(opt),
pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', opts)
)
end)
end

View File

@@ -26,7 +26,7 @@ describe('nvim_ui_attach()', function()
end)
it('validation', function()
eq('No such UI option: foo', pcall_err(api.nvim_ui_attach, 80, 24, { foo = { 'foo' } }))
eq("Invalid UI option: 'foo'", pcall_err(api.nvim_ui_attach, 80, 24, { foo = { 'foo' } }))
eq(
"Invalid 'ext_linegrid': expected Boolean, got Array",

View File

@@ -729,7 +729,7 @@ describe('API', function()
pcall_err(request, 'nvim_call_dict_function', "{ 'f': '' }", 'f', { 1, 2 })
)
eq(
'dict argument type must be String or Dict',
'Invalid dict argument: expected String or Dict',
pcall_err(request, 'nvim_call_dict_function', 42, 'f', { 1, 2 })
)
eq(
@@ -738,7 +738,7 @@ describe('API', function()
)
eq('dict not found', pcall_err(request, 'nvim_call_dict_function', '42', 'f', { 1, 2 }))
eq(
'Invalid (empty) function name',
'Invalid function name: (empty)',
pcall_err(request, 'nvim_call_dict_function', "{ 'f': '' }", '', { 1, 2 })
)
end)
@@ -3791,7 +3791,7 @@ describe('API', function()
'Invalid chunk: expected Array with 1 or 2 Strings',
pcall_err(api.nvim_echo, { { '', '', '' } }, 1, {})
)
eq('Invalid hl_group: text highlight', pcall_err(api.nvim_echo, { { '', false } }, 1, {}))
eq("Invalid 'hl_group': 'text highlight'", pcall_err(api.nvim_echo, { { '', false } }, 1, {}))
end)
it('should clear cmdline message before echo', function()

View File

@@ -2163,7 +2163,7 @@ describe('API/win', function()
it('no memory leak with valid title and invalid footer', function()
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_open_win, 0, false, {
relative = 'editor',
row = 10,
@@ -2179,7 +2179,7 @@ describe('API/win', function()
it('no memory leak with invalid title and valid footer', function()
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_open_win, 0, false, {
relative = 'editor',
row = 10,
@@ -2523,11 +2523,11 @@ describe('API/win', function()
eq('below', config.split)
eq(
"non-float with 'win' requires at least 'split' or 'vertical'",
"Required: non-float with 'win' requires 'split' or 'vertical'",
pcall_err(api.nvim_win_set_config, 0, { win = 0 })
)
eq(
"non-float with 'win' requires at least 'split' or 'vertical'",
"Required: non-float with 'win' requires 'split' or 'vertical'",
pcall_err(api.nvim_win_set_config, 0, { win = 0, relative = '' })
)
@@ -3310,7 +3310,7 @@ describe('API/win', function()
eq(true, pcall(api.nvim_win_set_config, win, cfg))
cfg.noautocmd = false
eq(
"'noautocmd' cannot be changed with existing windows",
"'noautocmd' cannot be changed on existing window",
pcall_err(api.nvim_win_set_config, win, cfg)
)
end)
@@ -3596,13 +3596,13 @@ describe('API/win', function()
border = 'single',
})
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_win_set_config, win, { title = 0 })
)
command('redraw!')
assert_alive()
eq(
'title/footer cannot be an empty array',
"Invalid 'title/footer': expected non-empty Array",
pcall_err(api.nvim_win_set_config, win, { title = {} })
)
command('redraw!')
@@ -3620,13 +3620,13 @@ describe('API/win', function()
border = 'single',
})
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_win_set_config, win, { footer = 0 })
)
command('redraw!')
assert_alive()
eq(
'title/footer cannot be an empty array',
"Invalid 'title/footer': expected non-empty Array",
pcall_err(api.nvim_win_set_config, win, { footer = {} })
)
command('redraw!')
@@ -3651,7 +3651,7 @@ describe('API/win', function()
it('with valid title and invalid footer', function()
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_win_set_config, win, {
title = { { 'NEW_TITLE' } },
footer = 0,
@@ -3664,7 +3664,7 @@ describe('API/win', function()
it('with invalid title and valid footer', function()
eq(
'title/footer must be string or array',
"Invalid 'title/footer': expected String or Array, got Integer",
pcall_err(api.nvim_win_set_config, win, {
title = 0,
footer = { { 'NEW_FOOTER' } },

View File

@@ -44,7 +44,7 @@ describe('luaeval(vim.api.…)', function()
it('transforms API error from nvim_win_set_cursor into lua error', function()
eq(
{ false, 'Argument "pos" must be a [row, col] array' },
{ false, "Invalid 'pos': expected [row, col] array" },
fn.luaeval('{pcall(vim.api.nvim_win_set_cursor, 0, {1, 2, 3})}')
)
-- Used to produce a memory leak due to a bug in nvim_win_set_cursor
@@ -58,7 +58,7 @@ describe('luaeval(vim.api.…)', function()
'transforms API error from nvim_win_set_cursor + same array as in first test into lua error',
function()
eq(
{ false, 'Argument "pos" must be a [row, col] array' },
{ false, "Invalid 'pos': expected [row, col] array" },
fn.luaeval('{pcall(vim.api.nvim_win_set_cursor, 0, {"b\\na"})}')
)
end

View File

@@ -2465,7 +2465,7 @@ describe('extmark decorations', function()
]])
eq(
'Invalid hl_group: hl_group item',
"Invalid 'hl_group': 'hl_group item'",
pcall_err(api.nvim_buf_set_extmark, 0, ns, 0, 0, { end_row = 1, hl_group = { 'Group1', 'Group2', { 'fail' } }, hl_eol = true })
)
end)

View File

@@ -44,6 +44,169 @@ describe('float window', function()
eq(1000, fn.win_getid())
end)
it('validation', function()
local buf = api.nvim_create_buf(false, false)
eq("Invalid key: 'bork'", pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, bork = true }))
eq("Required: 'relative' or 'external' when creating a float", pcall_err(api.nvim_open_win, buf, false, { win = 0 }))
eq(
"Conflict: 'vertical' not allowed with floating windows",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, vertical = true })
)
eq(
"Conflict: 'split' not allowed with floating windows",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, split = 'left' })
)
eq(
"Conflict: 'relative' not allowed with 'external'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, external = true })
)
eq(
"Invalid 'relative': 'shell'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'shell', row = 0, col = 0 })
)
eq(
"Invalid 'anchor': 'bottom'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, anchor = 'bottom' })
)
eq(
"Required: 'relative' requires 'row'/'col' or 'bufpos'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor' })
)
eq(
"Invalid 'width': expected positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = -1, height = 2, relative = 'editor', row = 0, col = 0 })
)
eq(
"Invalid 'height': expected positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = -1, relative = 'editor', row = 0, col = 0 })
)
eq(
"Invalid 'height': expected positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 0, relative = 'editor', row = 0, col = 0 })
)
eq("Required: 'width'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0 }))
eq("Required: 'height'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0, width = 2 }))
eq("Invalid 'split': 'up'", pcall_err(api.nvim_open_win, buf, false, { split = 'up' }))
eq(
"Invalid 'bufpos': expected [row, col] array",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, bufpos = { 0 } })
)
eq(
"Invalid 'zindex': expected positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, zindex = 0 })
)
eq(
"Invalid 'zindex': expected positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, zindex = -1 })
)
eq(
"Invalid 'style': 'bogus'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, style = 'bogus' })
)
eq(
"Invalid 'border': 'bogus'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = 'bogus' })
)
eq(
"Invalid 'border': expected 1, 2, 4, or 8 chars",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { '', '', '' } })
)
eq(
"Invalid 'border': expected 1 or 2-item Array",
pcall_err(
api.nvim_open_win,
buf,
false,
{ width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { { 'a', 'b', 'c' } } }
)
)
eq(
"Invalid 'border': expected Array of Strings",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { { 1 } } })
)
eq(
"Invalid 'border': expected String or Array, got Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { 42 } })
)
eq(
"Invalid 'border': expected only one-cell chars",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { 'aa' } })
)
eq(
"Invalid 'border': expected corner char between edge chars",
pcall_err(
api.nvim_open_win,
buf,
false,
{ width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = { '', '-', '', '|', '', '-', '', '|' } }
)
)
-- title_pos/footer_pos validation
eq(
"Invalid 'title_pos': 'bogus'",
pcall_err(
api.nvim_open_win,
buf,
false,
{ width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = 'single', title = 'T', title_pos = 'bogus' }
)
)
eq(
"Invalid 'footer_pos': 'bogus'",
pcall_err(
api.nvim_open_win,
buf,
false,
{ width = 20, height = 2, relative = 'editor', row = 0, col = 0, border = 'single', footer = 'F', footer_pos = 'bogus' }
)
)
eq(
"Required: 'footer' requires 'footer_pos'",
pcall_err(
api.nvim_open_win,
buf,
false,
{ relative = 'editor', width = 9, height = 2, row = 2, col = 5, border = 'single', footer_pos = 'left' }
)
)
eq(
"Required: 'title' requires 'title_pos'",
pcall_err(
api.nvim_open_win,
buf,
false,
{ relative = 'editor', width = 9, height = 2, row = 2, col = 5, border = 'single', title_pos = 'left' }
)
)
end)
it('validation: split window', function()
local buf = api.nvim_create_buf(false, true)
eq("Conflict: 'zindex' not allowed with non-float window", pcall_err(api.nvim_open_win, buf, false, { split = 'left', zindex = 100 }))
eq("Conflict: 'title' not allowed with non-float window", pcall_err(api.nvim_open_win, buf, false, { split = 'left', title = 'T' }))
eq("Conflict: 'footer' not allowed with non-float window", pcall_err(api.nvim_open_win, buf, false, { split = 'left', footer = 'F' }))
eq(
"Conflict: 'border' not allowed with non-float window",
pcall_err(api.nvim_open_win, buf, false, { split = 'left', border = 'single' })
)
eq("Conflict: 'row' not allowed with non-float window", pcall_err(api.nvim_open_win, buf, true, { split = 'right', row = 10 }))
eq("Conflict: 'col' not allowed with non-float window", pcall_err(api.nvim_open_win, buf, true, { split = 'right', col = 10 }))
eq(
"Conflict: 'bufpos' not allowed with non-float window",
pcall_err(api.nvim_open_win, buf, true, { split = 'right', bufpos = { 0, 0 } })
)
local winid = api.nvim_open_win(buf, true, { split = 'right' })
eq("Conflict: 'row' not allowed with non-float window", pcall_err(api.nvim_win_set_config, winid, { split = 'right', row = 10 }))
eq("Conflict: 'col' not allowed with non-float window", pcall_err(api.nvim_win_set_config, winid, { split = 'right', col = 10 }))
eq(
"Conflict: 'bufpos' not allowed with non-float window",
pcall_err(api.nvim_win_set_config, winid, { split = 'right', bufpos = { 0, 0 } })
)
end)
it('win_execute() should work', function()
local buf = api.nvim_create_buf(false, false)
api.nvim_buf_set_lines(buf, 0, -1, true, { 'the floatwin', 'abc', 'def' })
@@ -221,23 +384,12 @@ describe('float window', function()
eq({ 14, 12 }, { pos[1], pos[2] })
end)
it('error message when invalid field specified for split', function()
local bufnr = api.nvim_create_buf(false, true)
eq("non-float cannot have 'row'", pcall_err(api.nvim_open_win, bufnr, true, { split = 'right', row = 10 }))
eq("non-float cannot have 'col'", pcall_err(api.nvim_open_win, bufnr, true, { split = 'right', col = 10 }))
eq("non-float cannot have 'bufpos'", pcall_err(api.nvim_open_win, bufnr, true, { split = 'right', bufpos = { 0, 0 } }))
local winid = api.nvim_open_win(bufnr, true, { split = 'right' })
eq("non-float cannot have 'row'", pcall_err(api.nvim_win_set_config, winid, { split = 'right', row = 10 }))
eq("non-float cannot have 'col'", pcall_err(api.nvim_win_set_config, winid, { split = 'right', col = 10 }))
eq("non-float cannot have 'bufpos'", pcall_err(api.nvim_win_set_config, winid, { split = 'right', bufpos = { 0, 0 } }))
end)
it('error message when reconfig missing relative field', function()
it('error when reconfig missing relative field', function()
local bufnr = api.nvim_create_buf(false, true)
local opts = { width = 10, height = 10, col = 5, row = 5, relative = 'editor', style = 'minimal' }
local winid = api.nvim_open_win(bufnr, true, opts)
eq(
"Missing 'relative' field when reconfiguring floating window 1001",
"Required: 'relative' when reconfiguring floating window 1001",
pcall_err(api.nvim_win_set_config, winid, { width = 3, height = 3, row = 10, col = 10 })
)
end)
@@ -2093,23 +2245,7 @@ describe('float window', function()
end
end)
it('validates title title_pos', function()
local buf = api.nvim_create_buf(false, false)
eq(
'title_pos requires title to be set',
pcall_err(api.nvim_open_win, buf, false, {
relative = 'editor',
width = 9,
height = 2,
row = 2,
col = 5,
border = 'single',
title_pos = 'left',
})
)
end)
it('validate title_pos in nvim_win_get_config', function()
it('nvim_win_get_config.title_pos', function()
local title_pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, false)
local opts = {
@@ -2130,23 +2266,7 @@ describe('float window', function()
eq('center', title_pos)
end)
it('validates footer footer_pos', function()
local buf = api.nvim_create_buf(false, false)
eq(
'footer_pos requires footer to be set',
pcall_err(api.nvim_open_win, buf, false, {
relative = 'editor',
width = 9,
height = 2,
row = 2,
col = 5,
border = 'single',
footer_pos = 'left',
})
)
end)
it('validate footer_pos in nvim_win_get_config', function()
it('nvim_win_get_config.footer_pos', function()
local footer_pos = exec_lua([[
local bufnr = vim.api.nvim_create_buf(false, false)
local opts = {
@@ -3779,56 +3899,15 @@ describe('float window', function()
end)
end)
it('API has proper error messages', function()
local buf = api.nvim_create_buf(false, false)
eq("Invalid key: 'bork'", pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, bork = true }))
eq("Must specify 'relative' or 'external' when creating a float", pcall_err(api.nvim_open_win, buf, false, { win = 0 }))
eq(
"floating windows cannot have 'vertical'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, vertical = true })
)
eq(
"floating windows cannot have 'split'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, split = 'left' })
)
eq(
"Only one of 'relative' and 'external' must be used",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, external = true })
)
eq(
"Invalid value of 'relative' key",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'shell', row = 0, col = 0 })
)
eq(
"Invalid value of 'anchor' key",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, anchor = 'bottom' })
)
eq(
"'relative' requires 'row'/'col' or 'bufpos'",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor' })
)
eq(
"'width' key must be a positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = -1, height = 2, relative = 'editor', row = 0, col = 0 })
)
eq(
"'height' key must be a positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = -1, relative = 'editor', row = 0, col = 0 })
)
eq(
"'height' key must be a positive Integer",
pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 0, relative = 'editor', row = 0, col = 0 })
)
eq("Must specify 'width'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0 }))
eq("Must specify 'height'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0, width = 2 }))
it('validation: multigrid', function()
if multigrid then
local buf = api.nvim_create_buf(false, false)
eq(
"external window cannot have 'win'",
"Conflict: 'win' not allowed with external window",
pcall_err(api.nvim_open_win, buf, false, { external = true, win = 0, width = 10, height = 10 })
)
api.nvim_open_win(buf, true, { external = true, width = 10, height = 10 })
eq("external window cannot have 'win'", pcall_err(api.nvim_win_set_config, 0, { win = 0 }))
eq("Conflict: 'win' not allowed with external window", pcall_err(api.nvim_win_set_config, 0, { win = 0 }))
-- OK to include "win" if external window is also reconfigured to a normal float.
api.nvim_win_set_config(0, { relative = 'editor', win = 0, row = 0, col = 0, width = 5, height = 5 })
eq('editor', api.nvim_win_get_config(0).relative)

View File

@@ -535,7 +535,7 @@ describe('ui/ext_popupmenu', function()
it('an error occurs when ext_popupmenu is false', function()
api.nvim_ui_pum_set_height(1)
screen:set_option('ext_popupmenu', false)
eq('It must support the ext_popupmenu option', pcall_err(api.nvim_ui_pum_set_height, 1))
eq('UI must support the ext_popupmenu option', pcall_err(api.nvim_ui_pum_set_height, 1))
end)
end)