mirror of
https://github.com/neovim/neovim.git
synced 2025-09-07 11:58:17 +00:00
1682 lines
48 KiB
C
1682 lines
48 KiB
C
// This is an open source non-commercial project. Dear PVS-Studio, please check
|
|
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
|
|
|
|
#include <assert.h>
|
|
#include <inttypes.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "nvim/api/private/converter.h"
|
|
#include "nvim/api/private/defs.h"
|
|
#include "nvim/api/private/helpers.h"
|
|
#include "nvim/api/vim.h"
|
|
#include "nvim/ascii.h"
|
|
#include "nvim/assert.h"
|
|
#include "nvim/buffer.h"
|
|
#include "nvim/charset.h"
|
|
#include "nvim/decoration.h"
|
|
#include "nvim/eval.h"
|
|
#include "nvim/eval/typval.h"
|
|
#include "nvim/extmark.h"
|
|
#include "nvim/fileio.h"
|
|
#include "nvim/getchar.h"
|
|
#include "nvim/highlight_group.h"
|
|
#include "nvim/lib/kvec.h"
|
|
#include "nvim/lua/executor.h"
|
|
#include "nvim/map.h"
|
|
#include "nvim/map_defs.h"
|
|
#include "nvim/mark.h"
|
|
#include "nvim/memline.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/msgpack_rpc/helpers.h"
|
|
#include "nvim/option.h"
|
|
#include "nvim/option_defs.h"
|
|
#include "nvim/ui.h"
|
|
#include "nvim/version.h"
|
|
#include "nvim/vim.h"
|
|
#include "nvim/window.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "api/private/funcs_metadata.generated.h"
|
|
# include "api/private/helpers.c.generated.h"
|
|
# include "api/private/ui_events_metadata.generated.h"
|
|
#endif
|
|
|
|
/// Start block that may cause VimL exceptions while evaluating another code
|
|
///
|
|
/// Used when caller is supposed to be operating when other VimL code is being
|
|
/// processed and that “other VimL code” must not be affected.
|
|
///
|
|
/// @param[out] tstate Location where try state should be saved.
|
|
void try_enter(TryState *const tstate)
|
|
{
|
|
// TODO(ZyX-I): Check whether try_enter()/try_leave() may use
|
|
// enter_cleanup()/leave_cleanup(). Or
|
|
// save_dbg_stuff()/restore_dbg_stuff().
|
|
*tstate = (TryState) {
|
|
.current_exception = current_exception,
|
|
.msg_list = (const struct msglist *const *)msg_list,
|
|
.private_msg_list = NULL,
|
|
.trylevel = trylevel,
|
|
.got_int = got_int,
|
|
.need_rethrow = need_rethrow,
|
|
.did_emsg = did_emsg,
|
|
};
|
|
msg_list = &tstate->private_msg_list;
|
|
current_exception = NULL;
|
|
trylevel = 1;
|
|
got_int = false;
|
|
need_rethrow = false;
|
|
did_emsg = false;
|
|
}
|
|
|
|
/// End try block, set the error message if any and restore previous state
|
|
///
|
|
/// @warning Return is consistent with most functions (false on error), not with
|
|
/// try_end (true on error).
|
|
///
|
|
/// @param[in] tstate Previous state to restore.
|
|
/// @param[out] err Location where error should be saved.
|
|
///
|
|
/// @return false if error occurred, true otherwise.
|
|
bool try_leave(const TryState *const tstate, Error *const err)
|
|
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
const bool ret = !try_end(err);
|
|
assert(trylevel == 0);
|
|
assert(!need_rethrow);
|
|
assert(!got_int);
|
|
assert(!did_emsg);
|
|
assert(msg_list == &tstate->private_msg_list);
|
|
assert(*msg_list == NULL);
|
|
assert(current_exception == NULL);
|
|
msg_list = (struct msglist **)tstate->msg_list;
|
|
current_exception = tstate->current_exception;
|
|
trylevel = tstate->trylevel;
|
|
got_int = tstate->got_int;
|
|
need_rethrow = tstate->need_rethrow;
|
|
did_emsg = tstate->did_emsg;
|
|
return ret;
|
|
}
|
|
|
|
/// Start block that may cause vimscript exceptions
|
|
///
|
|
/// Each try_start() call should be mirrored by try_end() call.
|
|
///
|
|
/// To be used as a replacement of `:try … catch … endtry` in C code, in cases
|
|
/// when error flag could not already be set. If there may be pending error
|
|
/// state at the time try_start() is executed which needs to be preserved,
|
|
/// try_enter()/try_leave() pair should be used instead.
|
|
void try_start(void)
|
|
{
|
|
trylevel++;
|
|
}
|
|
|
|
/// End try block, set the error message if any and return true if an error
|
|
/// occurred.
|
|
///
|
|
/// @param err Pointer to the stack-allocated error object
|
|
/// @return true if an error occurred
|
|
bool try_end(Error *err)
|
|
{
|
|
// Note: all globals manipulated here should be saved/restored in
|
|
// try_enter/try_leave.
|
|
trylevel--;
|
|
|
|
// Set by emsg(), affects aborting(). See also enter_cleanup().
|
|
did_emsg = false;
|
|
force_abort = false;
|
|
|
|
if (got_int) {
|
|
if (current_exception) {
|
|
// If we got an interrupt, discard the current exception
|
|
discard_current_exception();
|
|
}
|
|
|
|
api_set_error(err, kErrorTypeException, "Keyboard interrupt");
|
|
got_int = false;
|
|
} else if (msg_list != NULL && *msg_list != NULL) {
|
|
int should_free;
|
|
char *msg = get_exception_string(*msg_list,
|
|
ET_ERROR,
|
|
NULL,
|
|
&should_free);
|
|
api_set_error(err, kErrorTypeException, "%s", msg);
|
|
free_global_msglist();
|
|
|
|
if (should_free) {
|
|
xfree(msg);
|
|
}
|
|
} else if (current_exception) {
|
|
api_set_error(err, kErrorTypeException, "%s", current_exception->value);
|
|
discard_current_exception();
|
|
}
|
|
|
|
return ERROR_SET(err);
|
|
}
|
|
|
|
/// Recursively expands a vimscript value in a dict
|
|
///
|
|
/// @param dict The vimscript dict
|
|
/// @param key The key
|
|
/// @param[out] err Details of an error that may have occurred
|
|
Object dict_get_value(dict_T *dict, String key, 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);
|
|
return (Object)OBJECT_INIT;
|
|
}
|
|
|
|
return vim_to_object(&di->di_tv);
|
|
}
|
|
|
|
dictitem_T *dict_check_writable(dict_T *dict, String key, bool del, Error *err)
|
|
{
|
|
dictitem_T *di = tv_dict_find(dict, key.data, (ptrdiff_t)key.size);
|
|
|
|
if (di != NULL) {
|
|
if (di->di_flags & DI_FLAGS_RO) {
|
|
api_set_error(err, kErrorTypeException, "Key is read-only: %s", key.data);
|
|
} else if (di->di_flags & DI_FLAGS_LOCK) {
|
|
api_set_error(err, kErrorTypeException, "Key is locked: %s", key.data);
|
|
} else if (del && (di->di_flags & DI_FLAGS_FIX)) {
|
|
api_set_error(err, kErrorTypeException, "Key is fixed: %s", key.data);
|
|
}
|
|
} else if (dict->dv_lock) {
|
|
api_set_error(err, kErrorTypeException, "Dictionary is locked");
|
|
} else if (key.size == 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Key name is empty");
|
|
} else if (key.size > INT_MAX) {
|
|
api_set_error(err, kErrorTypeValidation, "Key name is too long");
|
|
}
|
|
|
|
return di;
|
|
}
|
|
|
|
/// Set a value in a scope dict. Objects are recursively expanded into their
|
|
/// vimscript equivalents.
|
|
///
|
|
/// @param dict The vimscript dict
|
|
/// @param key The key
|
|
/// @param value The new value
|
|
/// @param del Delete key in place of setting it. Argument `value` is ignored in
|
|
/// this case.
|
|
/// @param retval If true the old value will be converted and returned.
|
|
/// @param[out] err Details of an error that may have occurred
|
|
/// @return The old value if `retval` is true and the key was present, else NIL
|
|
Object dict_set_var(dict_T *dict, String key, Object value, bool del, bool retval, Error *err)
|
|
{
|
|
Object rv = OBJECT_INIT;
|
|
dictitem_T *di = dict_check_writable(dict, key, del, err);
|
|
|
|
if (ERROR_SET(err)) {
|
|
return rv;
|
|
}
|
|
|
|
if (del) {
|
|
// Delete the key
|
|
if (di == NULL) {
|
|
// Doesn't exist, fail
|
|
api_set_error(err, kErrorTypeValidation, "Key not found: %s",
|
|
key.data);
|
|
} else {
|
|
// Return the old value
|
|
if (retval) {
|
|
rv = vim_to_object(&di->di_tv);
|
|
}
|
|
// Delete the entry
|
|
tv_dict_item_remove(dict, di);
|
|
}
|
|
} else {
|
|
// Update the key
|
|
typval_T tv;
|
|
|
|
// Convert the object to a vimscript type in the temporary variable
|
|
if (!object_to_vim(value, &tv, err)) {
|
|
return rv;
|
|
}
|
|
|
|
if (di == NULL) {
|
|
// Need to create an entry
|
|
di = tv_dict_item_alloc_len(key.data, key.size);
|
|
tv_dict_add(dict, di);
|
|
} else {
|
|
// Return the old value
|
|
if (retval) {
|
|
rv = vim_to_object(&di->di_tv);
|
|
}
|
|
tv_clear(&di->di_tv);
|
|
}
|
|
|
|
// Update the value
|
|
tv_copy(&tv, &di->di_tv);
|
|
// Clear the temporary variable
|
|
tv_clear(&tv);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
/// Gets the value of a global or local(buffer, window) option.
|
|
///
|
|
/// @param from If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer
|
|
/// to the window or buffer.
|
|
/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF`
|
|
/// @param name The option name
|
|
/// @param[out] err Details of an error that may have occurred
|
|
/// @return the option value
|
|
Object get_option_from(void *from, int type, String name, Error *err)
|
|
{
|
|
Object rv = OBJECT_INIT;
|
|
|
|
if (name.size == 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Empty option name");
|
|
return rv;
|
|
}
|
|
|
|
// Return values
|
|
int64_t numval;
|
|
char *stringval = NULL;
|
|
int flags = get_option_value_strict(name.data, &numval, &stringval,
|
|
type, from);
|
|
|
|
if (!flags) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid option name: '%s'",
|
|
name.data);
|
|
return rv;
|
|
}
|
|
|
|
if (flags & SOPT_BOOL) {
|
|
rv.type = kObjectTypeBoolean;
|
|
rv.data.boolean = numval ? true : false;
|
|
} else if (flags & SOPT_NUM) {
|
|
rv.type = kObjectTypeInteger;
|
|
rv.data.integer = numval;
|
|
} else if (flags & SOPT_STRING) {
|
|
if (stringval) {
|
|
rv.type = kObjectTypeString;
|
|
rv.data.string.data = stringval;
|
|
rv.data.string.size = strlen(stringval);
|
|
} else {
|
|
api_set_error(err, kErrorTypeException,
|
|
"Failed to get value for option '%s'",
|
|
name.data);
|
|
}
|
|
} else {
|
|
api_set_error(err,
|
|
kErrorTypeException,
|
|
"Unknown type for option '%s'",
|
|
name.data);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
/// Sets the value of a global or local(buffer, window) option.
|
|
///
|
|
/// @param to If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer
|
|
/// to the window or buffer.
|
|
/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF`
|
|
/// @param name The option name
|
|
/// @param[out] err Details of an error that may have occurred
|
|
void set_option_to(uint64_t channel_id, void *to, int type, String name, Object value, Error *err)
|
|
{
|
|
if (name.size == 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Empty option name");
|
|
return;
|
|
}
|
|
|
|
int flags = get_option_value_strict(name.data, NULL, NULL, type, to);
|
|
|
|
if (flags == 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid option name '%s'",
|
|
name.data);
|
|
return;
|
|
}
|
|
|
|
if (value.type == kObjectTypeNil) {
|
|
if (type == SREQ_GLOBAL) {
|
|
api_set_error(err, kErrorTypeException, "Cannot unset option '%s'",
|
|
name.data);
|
|
return;
|
|
} else if (!(flags & SOPT_GLOBAL)) {
|
|
api_set_error(err,
|
|
kErrorTypeException,
|
|
"Cannot unset option '%s' "
|
|
"because it doesn't have a global value",
|
|
name.data);
|
|
return;
|
|
} else {
|
|
unset_global_local_option(name.data, to);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int numval = 0;
|
|
char *stringval = NULL;
|
|
|
|
if (flags & SOPT_BOOL) {
|
|
if (value.type != kObjectTypeBoolean) {
|
|
api_set_error(err,
|
|
kErrorTypeValidation,
|
|
"Option '%s' requires a Boolean value",
|
|
name.data);
|
|
return;
|
|
}
|
|
|
|
numval = value.data.boolean;
|
|
} else if (flags & SOPT_NUM) {
|
|
if (value.type != kObjectTypeInteger) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Option '%s' requires an integer value",
|
|
name.data);
|
|
return;
|
|
}
|
|
|
|
if (value.data.integer > INT_MAX || value.data.integer < INT_MIN) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Value for option '%s' is out of range",
|
|
name.data);
|
|
return;
|
|
}
|
|
|
|
numval = (int)value.data.integer;
|
|
} else {
|
|
if (value.type != kObjectTypeString) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Option '%s' requires a string value",
|
|
name.data);
|
|
return;
|
|
}
|
|
|
|
stringval = value.data.string.data;
|
|
}
|
|
|
|
WITH_SCRIPT_CONTEXT(channel_id, {
|
|
const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL))
|
|
? 0 : (type == SREQ_GLOBAL)
|
|
? OPT_GLOBAL : OPT_LOCAL;
|
|
|
|
set_option_value_for(name.data, numval, stringval,
|
|
opt_flags, type, to, err);
|
|
});
|
|
}
|
|
|
|
|
|
buf_T *find_buffer_by_handle(Buffer buffer, Error *err)
|
|
{
|
|
if (buffer == 0) {
|
|
return curbuf;
|
|
}
|
|
|
|
buf_T *rv = handle_get_buffer(buffer);
|
|
|
|
if (!rv) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid buffer id: %d", buffer);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
win_T *find_window_by_handle(Window window, Error *err)
|
|
{
|
|
if (window == 0) {
|
|
return curwin;
|
|
}
|
|
|
|
win_T *rv = handle_get_window(window);
|
|
|
|
if (!rv) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid window id: %d", window);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
tabpage_T *find_tab_by_handle(Tabpage tabpage, Error *err)
|
|
{
|
|
if (tabpage == 0) {
|
|
return curtab;
|
|
}
|
|
|
|
tabpage_T *rv = handle_get_tabpage(tabpage);
|
|
|
|
if (!rv) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid tabpage id: %d", tabpage);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
/// Allocates a String consisting of a single char. Does not support multibyte
|
|
/// characters. The resulting string is also NUL-terminated, to facilitate
|
|
/// interoperating with code using C strings.
|
|
///
|
|
/// @param char the char to convert
|
|
/// @return the resulting String, if the input char was NUL, an
|
|
/// empty String is returned
|
|
String cchar_to_string(char c)
|
|
{
|
|
char buf[] = { c, NUL };
|
|
return (String){
|
|
.data = xmemdupz(buf, 1),
|
|
.size = (c != NUL) ? 1 : 0
|
|
};
|
|
}
|
|
|
|
/// Copies a C string into a String (binary safe string, characters + length).
|
|
/// The resulting string is also NUL-terminated, to facilitate interoperating
|
|
/// with code using C strings.
|
|
///
|
|
/// @param str the C string to copy
|
|
/// @return the resulting String, if the input string was NULL, an
|
|
/// empty String is returned
|
|
String cstr_to_string(const char *str)
|
|
{
|
|
if (str == NULL) {
|
|
return (String)STRING_INIT;
|
|
}
|
|
|
|
size_t len = strlen(str);
|
|
return (String){
|
|
.data = xmemdupz(str, len),
|
|
.size = len,
|
|
};
|
|
}
|
|
|
|
/// Copies buffer to an allocated String.
|
|
/// The resulting string is also NUL-terminated, to facilitate interoperating
|
|
/// with code using C strings.
|
|
///
|
|
/// @param buf the buffer to copy
|
|
/// @param size length of the buffer
|
|
/// @return the resulting String, if the input string was NULL, an
|
|
/// empty String is returned
|
|
String cbuf_to_string(const char *buf, size_t size)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
return (String){
|
|
.data = xmemdupz(buf, size),
|
|
.size = size
|
|
};
|
|
}
|
|
|
|
String cstrn_to_string(const char *str, size_t maxsize)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
return cbuf_to_string(str, STRNLEN(str, maxsize));
|
|
}
|
|
|
|
/// Creates a String using the given C string. Unlike
|
|
/// cstr_to_string this function DOES NOT copy the C string.
|
|
///
|
|
/// @param str the C string to use
|
|
/// @return The resulting String, or an empty String if
|
|
/// str was NULL
|
|
String cstr_as_string(char *str) FUNC_ATTR_PURE
|
|
{
|
|
if (str == NULL) {
|
|
return (String)STRING_INIT;
|
|
}
|
|
return (String){ .data = str, .size = strlen(str) };
|
|
}
|
|
|
|
/// Return the owned memory of a ga as a String
|
|
///
|
|
/// Reinitializes the ga to a valid empty state.
|
|
String ga_take_string(garray_T *ga)
|
|
{
|
|
String str = { .data = (char *)ga->ga_data, .size = (size_t)ga->ga_len };
|
|
ga->ga_data = NULL;
|
|
ga->ga_len = 0;
|
|
ga->ga_maxlen = 0;
|
|
return str;
|
|
}
|
|
|
|
/// Creates "readfile()-style" ArrayOf(String) from a binary string.
|
|
///
|
|
/// - Lines break at \n (NL/LF/line-feed).
|
|
/// - NUL bytes are replaced with NL.
|
|
/// - If the last byte is a linebreak an extra empty list item is added.
|
|
///
|
|
/// @param input Binary string
|
|
/// @param crlf Also break lines at CR and CRLF.
|
|
/// @return [allocated] String array
|
|
Array string_to_array(const String input, bool crlf)
|
|
{
|
|
Array ret = ARRAY_DICT_INIT;
|
|
for (size_t i = 0; i < input.size; i++) {
|
|
const char *start = input.data + i;
|
|
const char *end = start;
|
|
size_t line_len = 0;
|
|
for (; line_len < input.size - i; line_len++) {
|
|
end = start + line_len;
|
|
if (*end == NL || (crlf && *end == CAR)) {
|
|
break;
|
|
}
|
|
}
|
|
i += line_len;
|
|
if (crlf && *end == CAR && i + 1 < input.size && *(end + 1) == NL) {
|
|
i += 1; // Advance past CRLF.
|
|
}
|
|
String s = {
|
|
.size = line_len,
|
|
.data = xmemdupz(start, line_len),
|
|
};
|
|
memchrsub(s.data, NUL, NL, line_len);
|
|
ADD(ret, STRING_OBJ(s));
|
|
// If line ends at end-of-buffer, add empty final item.
|
|
// This is "readfile()-style", see also ":help channel-lines".
|
|
if (i + 1 == input.size && (*end == NL || (crlf && *end == CAR))) {
|
|
ADD(ret, STRING_OBJ(STRING_INIT));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// Set, tweak, or remove a mapping in a mode. Acts as the implementation for
|
|
/// functions like @ref nvim_buf_set_keymap.
|
|
///
|
|
/// Arguments are handled like @ref nvim_set_keymap unless noted.
|
|
/// @param buffer Buffer handle for a specific buffer, or 0 for the current
|
|
/// buffer, or -1 to signify global behavior ("all buffers")
|
|
/// @param is_unmap When true, removes the mapping that matches {lhs}.
|
|
void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mode, String lhs,
|
|
String rhs, Dict(keymap) *opts, Error *err)
|
|
{
|
|
LuaRef lua_funcref = LUA_NOREF;
|
|
bool global = (buffer == -1);
|
|
if (global) {
|
|
buffer = 0;
|
|
}
|
|
buf_T *target_buf = find_buffer_by_handle(buffer, err);
|
|
|
|
if (!target_buf) {
|
|
return;
|
|
}
|
|
|
|
const sctx_T save_current_sctx = api_set_sctx(channel_id);
|
|
|
|
if (opts != NULL && opts->callback.type == kObjectTypeLuaRef) {
|
|
lua_funcref = opts->callback.data.luaref;
|
|
opts->callback.data.luaref = LUA_NOREF;
|
|
}
|
|
MapArguments parsed_args = MAP_ARGUMENTS_INIT;
|
|
if (opts) {
|
|
#define KEY_TO_BOOL(name) \
|
|
parsed_args.name = api_object_to_bool(opts->name, #name, false, err); \
|
|
if (ERROR_SET(err)) { \
|
|
goto fail_and_free; \
|
|
}
|
|
|
|
KEY_TO_BOOL(nowait);
|
|
KEY_TO_BOOL(noremap);
|
|
KEY_TO_BOOL(silent);
|
|
KEY_TO_BOOL(script);
|
|
KEY_TO_BOOL(expr);
|
|
KEY_TO_BOOL(unique);
|
|
#undef KEY_TO_BOOL
|
|
}
|
|
parsed_args.buffer = !global;
|
|
|
|
set_maparg_lhs_rhs((char_u *)lhs.data, lhs.size,
|
|
(char_u *)rhs.data, rhs.size, lua_funcref,
|
|
CPO_TO_CPO_FLAGS, &parsed_args);
|
|
if (opts != NULL && opts->desc.type == kObjectTypeString) {
|
|
parsed_args.desc = xstrdup(opts->desc.data.string.data);
|
|
} else {
|
|
parsed_args.desc = NULL;
|
|
}
|
|
if (parsed_args.lhs_len > MAXMAPLEN) {
|
|
api_set_error(err, kErrorTypeValidation, "LHS exceeds maximum map length: %s", lhs.data);
|
|
goto fail_and_free;
|
|
}
|
|
|
|
if (mode.size > 1) {
|
|
api_set_error(err, kErrorTypeValidation, "Shortname is too long: %s", mode.data);
|
|
goto fail_and_free;
|
|
}
|
|
int mode_val; // integer value of the mapping mode, to be passed to do_map()
|
|
char_u *p = (char_u *)((mode.size) ? mode.data : "m");
|
|
if (STRNCMP(p, "!", 2) == 0) {
|
|
mode_val = get_map_mode(&p, true); // mapmode-ic
|
|
} else {
|
|
mode_val = get_map_mode(&p, false);
|
|
if ((mode_val == VISUAL + SELECTMODE + NORMAL + OP_PENDING)
|
|
&& mode.size > 0) {
|
|
// get_map_mode() treats unrecognized mode shortnames as ":map".
|
|
// This is an error unless the given shortname was empty string "".
|
|
api_set_error(err, kErrorTypeValidation, "Invalid mode shortname: \"%s\"", (char *)p);
|
|
goto fail_and_free;
|
|
}
|
|
}
|
|
|
|
if (parsed_args.lhs_len == 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid (empty) LHS");
|
|
goto fail_and_free;
|
|
}
|
|
|
|
bool is_noremap = parsed_args.noremap;
|
|
assert(!(is_unmap && is_noremap));
|
|
|
|
if (!is_unmap && lua_funcref == LUA_NOREF
|
|
&& (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) {
|
|
if (rhs.size == 0) { // assume that the user wants RHS to be a <Nop>
|
|
parsed_args.rhs_is_noop = true;
|
|
} else {
|
|
// the given RHS was nonempty and not a <Nop>, but was parsed as if it
|
|
// were empty?
|
|
assert(false && "Failed to parse nonempty RHS!");
|
|
api_set_error(err, kErrorTypeValidation, "Parsing of nonempty RHS failed: %s", rhs.data);
|
|
goto fail_and_free;
|
|
}
|
|
} else if (is_unmap && (parsed_args.rhs_len || parsed_args.rhs_lua != LUA_NOREF)) {
|
|
if (parsed_args.rhs_len) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Gave nonempty RHS in unmap command: %s", parsed_args.rhs);
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation, "Gave nonempty RHS for unmap");
|
|
}
|
|
goto fail_and_free;
|
|
}
|
|
|
|
// buf_do_map() reads noremap/unmap as its own argument.
|
|
int maptype_val = 0;
|
|
if (is_unmap) {
|
|
maptype_val = 1;
|
|
} else if (is_noremap) {
|
|
maptype_val = 2;
|
|
}
|
|
|
|
switch (buf_do_map(maptype_val, &parsed_args, mode_val, 0, target_buf)) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
api_set_error(err, kErrorTypeException, (char *)e_invarg, 0);
|
|
goto fail_and_free;
|
|
case 2:
|
|
api_set_error(err, kErrorTypeException, (char *)e_nomap, 0);
|
|
goto fail_and_free;
|
|
case 5:
|
|
api_set_error(err, kErrorTypeException,
|
|
"E227: mapping already exists for %s", parsed_args.lhs);
|
|
goto fail_and_free;
|
|
default:
|
|
assert(false && "Unrecognized return code!");
|
|
goto fail_and_free;
|
|
} // switch
|
|
|
|
parsed_args.rhs_lua = LUA_NOREF; // don't clear ref on success
|
|
fail_and_free:
|
|
current_sctx = save_current_sctx;
|
|
NLUA_CLEAR_REF(parsed_args.rhs_lua);
|
|
xfree(parsed_args.rhs);
|
|
xfree(parsed_args.orig_rhs);
|
|
XFREE_CLEAR(parsed_args.desc);
|
|
}
|
|
|
|
/// Collects `n` buffer lines into array `l`, optionally replacing newlines
|
|
/// with NUL.
|
|
///
|
|
/// @param buf Buffer to get lines from
|
|
/// @param n Number of lines to collect
|
|
/// @param replace_nl Replace newlines ("\n") with NUL
|
|
/// @param start Line number to start from
|
|
/// @param[out] l Lines are copied here
|
|
/// @param err[out] Error, if any
|
|
/// @return true unless `err` was set
|
|
bool buf_collect_lines(buf_T *buf, size_t n, int64_t start, bool replace_nl, Array *l, Error *err)
|
|
{
|
|
for (size_t i = 0; i < n; i++) {
|
|
int64_t lnum = start + (int64_t)i;
|
|
|
|
if (lnum >= MAXLNUM) {
|
|
if (err != NULL) {
|
|
api_set_error(err, kErrorTypeValidation, "Line index is too high");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false);
|
|
Object str = STRING_OBJ(cstr_to_string(bufstr));
|
|
|
|
if (replace_nl) {
|
|
// Vim represents NULs as NLs, but this may confuse clients.
|
|
strchrsub(str.data.string.data, '\n', '\0');
|
|
}
|
|
|
|
l->items[i] = str;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Returns a substring of a buffer line
|
|
///
|
|
/// @param buf Buffer handle
|
|
/// @param lnum Line number (1-based)
|
|
/// @param start_col Starting byte offset into line (0-based)
|
|
/// @param end_col Ending byte offset into line (0-based, exclusive)
|
|
/// @param replace_nl Replace newlines ('\n') with null ('\0')
|
|
/// @param err Error object
|
|
/// @return The text between start_col and end_col on line lnum of buffer buf
|
|
String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col, bool replace_nl,
|
|
Error *err)
|
|
{
|
|
String rv = STRING_INIT;
|
|
|
|
if (lnum >= MAXLNUM) {
|
|
api_set_error(err, kErrorTypeValidation, "Line index is too high");
|
|
return rv;
|
|
}
|
|
|
|
const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false);
|
|
size_t line_length = strlen(bufstr);
|
|
|
|
start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col;
|
|
end_col = end_col < 0 ? (int64_t)line_length + end_col + 1 : end_col;
|
|
|
|
if (start_col >= MAXCOL || end_col >= MAXCOL) {
|
|
api_set_error(err, kErrorTypeValidation, "Column index is too high");
|
|
return rv;
|
|
}
|
|
|
|
if (start_col > end_col) {
|
|
api_set_error(err, kErrorTypeValidation, "start_col must be less than end_col");
|
|
return rv;
|
|
}
|
|
|
|
if ((size_t)start_col >= line_length) {
|
|
return rv;
|
|
}
|
|
|
|
rv = cstrn_to_string(&bufstr[start_col], (size_t)(end_col - start_col));
|
|
if (replace_nl) {
|
|
strchrsub(rv.data, '\n', '\0');
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
void api_free_string(String value)
|
|
{
|
|
if (!value.data) {
|
|
return;
|
|
}
|
|
|
|
xfree(value.data);
|
|
}
|
|
|
|
void api_free_object(Object value)
|
|
{
|
|
switch (value.type) {
|
|
case kObjectTypeNil:
|
|
case kObjectTypeBoolean:
|
|
case kObjectTypeInteger:
|
|
case kObjectTypeFloat:
|
|
case kObjectTypeBuffer:
|
|
case kObjectTypeWindow:
|
|
case kObjectTypeTabpage:
|
|
break;
|
|
|
|
case kObjectTypeString:
|
|
api_free_string(value.data.string);
|
|
break;
|
|
|
|
case kObjectTypeArray:
|
|
api_free_array(value.data.array);
|
|
break;
|
|
|
|
case kObjectTypeDictionary:
|
|
api_free_dictionary(value.data.dictionary);
|
|
break;
|
|
|
|
case kObjectTypeLuaRef:
|
|
api_free_luaref(value.data.luaref);
|
|
break;
|
|
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
|
|
void api_free_array(Array value)
|
|
{
|
|
for (size_t i = 0; i < value.size; i++) {
|
|
api_free_object(value.items[i]);
|
|
}
|
|
|
|
xfree(value.items);
|
|
}
|
|
|
|
void api_free_dictionary(Dictionary value)
|
|
{
|
|
for (size_t i = 0; i < value.size; i++) {
|
|
api_free_string(value.items[i].key);
|
|
api_free_object(value.items[i].value);
|
|
}
|
|
|
|
xfree(value.items);
|
|
}
|
|
|
|
void api_clear_error(Error *value)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
if (!ERROR_SET(value)) {
|
|
return;
|
|
}
|
|
xfree(value->msg);
|
|
value->msg = NULL;
|
|
value->type = kErrorTypeNone;
|
|
}
|
|
|
|
Dictionary api_metadata(void)
|
|
{
|
|
static Dictionary metadata = ARRAY_DICT_INIT;
|
|
|
|
if (!metadata.size) {
|
|
PUT(metadata, "version", DICTIONARY_OBJ(version_dict()));
|
|
init_function_metadata(&metadata);
|
|
init_ui_event_metadata(&metadata);
|
|
init_error_type_metadata(&metadata);
|
|
init_type_metadata(&metadata);
|
|
}
|
|
|
|
return copy_object(DICTIONARY_OBJ(metadata)).data.dictionary;
|
|
}
|
|
|
|
static void init_function_metadata(Dictionary *metadata)
|
|
{
|
|
msgpack_unpacked unpacked;
|
|
msgpack_unpacked_init(&unpacked);
|
|
if (msgpack_unpack_next(&unpacked,
|
|
(const char *)funcs_metadata,
|
|
sizeof(funcs_metadata),
|
|
NULL) != MSGPACK_UNPACK_SUCCESS) {
|
|
abort();
|
|
}
|
|
Object functions;
|
|
msgpack_rpc_to_object(&unpacked.data, &functions);
|
|
msgpack_unpacked_destroy(&unpacked);
|
|
PUT(*metadata, "functions", functions);
|
|
}
|
|
|
|
static void init_ui_event_metadata(Dictionary *metadata)
|
|
{
|
|
msgpack_unpacked unpacked;
|
|
msgpack_unpacked_init(&unpacked);
|
|
if (msgpack_unpack_next(&unpacked,
|
|
(const char *)ui_events_metadata,
|
|
sizeof(ui_events_metadata),
|
|
NULL) != MSGPACK_UNPACK_SUCCESS) {
|
|
abort();
|
|
}
|
|
Object ui_events;
|
|
msgpack_rpc_to_object(&unpacked.data, &ui_events);
|
|
msgpack_unpacked_destroy(&unpacked);
|
|
PUT(*metadata, "ui_events", ui_events);
|
|
Array ui_options = ARRAY_DICT_INIT;
|
|
ADD(ui_options, STRING_OBJ(cstr_to_string("rgb")));
|
|
for (UIExtension i = 0; i < kUIExtCount; i++) {
|
|
if (ui_ext_names[i][0] != '_') {
|
|
ADD(ui_options, STRING_OBJ(cstr_to_string(ui_ext_names[i])));
|
|
}
|
|
}
|
|
PUT(*metadata, "ui_options", ARRAY_OBJ(ui_options));
|
|
}
|
|
|
|
static void init_error_type_metadata(Dictionary *metadata)
|
|
{
|
|
Dictionary types = ARRAY_DICT_INIT;
|
|
|
|
Dictionary exception_metadata = ARRAY_DICT_INIT;
|
|
PUT(exception_metadata, "id", INTEGER_OBJ(kErrorTypeException));
|
|
|
|
Dictionary validation_metadata = ARRAY_DICT_INIT;
|
|
PUT(validation_metadata, "id", INTEGER_OBJ(kErrorTypeValidation));
|
|
|
|
PUT(types, "Exception", DICTIONARY_OBJ(exception_metadata));
|
|
PUT(types, "Validation", DICTIONARY_OBJ(validation_metadata));
|
|
|
|
PUT(*metadata, "error_types", DICTIONARY_OBJ(types));
|
|
}
|
|
|
|
static void init_type_metadata(Dictionary *metadata)
|
|
{
|
|
Dictionary types = ARRAY_DICT_INIT;
|
|
|
|
Dictionary buffer_metadata = ARRAY_DICT_INIT;
|
|
PUT(buffer_metadata, "id",
|
|
INTEGER_OBJ(kObjectTypeBuffer - EXT_OBJECT_TYPE_SHIFT));
|
|
PUT(buffer_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_buf_")));
|
|
|
|
Dictionary window_metadata = ARRAY_DICT_INIT;
|
|
PUT(window_metadata, "id",
|
|
INTEGER_OBJ(kObjectTypeWindow - EXT_OBJECT_TYPE_SHIFT));
|
|
PUT(window_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_win_")));
|
|
|
|
Dictionary tabpage_metadata = ARRAY_DICT_INIT;
|
|
PUT(tabpage_metadata, "id",
|
|
INTEGER_OBJ(kObjectTypeTabpage - EXT_OBJECT_TYPE_SHIFT));
|
|
PUT(tabpage_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_tabpage_")));
|
|
|
|
PUT(types, "Buffer", DICTIONARY_OBJ(buffer_metadata));
|
|
PUT(types, "Window", DICTIONARY_OBJ(window_metadata));
|
|
PUT(types, "Tabpage", DICTIONARY_OBJ(tabpage_metadata));
|
|
|
|
PUT(*metadata, "types", DICTIONARY_OBJ(types));
|
|
}
|
|
|
|
String copy_string(String str)
|
|
{
|
|
if (str.data != NULL) {
|
|
return (String){ .data = xmemdupz(str.data, str.size), .size = str.size };
|
|
} else {
|
|
return (String)STRING_INIT;
|
|
}
|
|
}
|
|
|
|
Array copy_array(Array array)
|
|
{
|
|
Array rv = ARRAY_DICT_INIT;
|
|
for (size_t i = 0; i < array.size; i++) {
|
|
ADD(rv, copy_object(array.items[i]));
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
Dictionary copy_dictionary(Dictionary dict)
|
|
{
|
|
Dictionary rv = ARRAY_DICT_INIT;
|
|
for (size_t i = 0; i < dict.size; i++) {
|
|
KeyValuePair item = dict.items[i];
|
|
PUT(rv, item.key.data, copy_object(item.value));
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
/// Creates a deep clone of an object
|
|
Object copy_object(Object obj)
|
|
{
|
|
switch (obj.type) {
|
|
case kObjectTypeBuffer:
|
|
case kObjectTypeTabpage:
|
|
case kObjectTypeWindow:
|
|
case kObjectTypeNil:
|
|
case kObjectTypeBoolean:
|
|
case kObjectTypeInteger:
|
|
case kObjectTypeFloat:
|
|
return obj;
|
|
|
|
case kObjectTypeString:
|
|
return STRING_OBJ(copy_string(obj.data.string));
|
|
|
|
case kObjectTypeArray:
|
|
return ARRAY_OBJ(copy_array(obj.data.array));
|
|
|
|
case kObjectTypeDictionary:
|
|
return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary));
|
|
|
|
case kObjectTypeLuaRef:
|
|
return LUAREF_OBJ(api_new_luaref(obj.data.luaref));
|
|
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
|
|
static void set_option_value_for(char *key, int numval, char *stringval, int opt_flags,
|
|
int opt_type, void *from, Error *err)
|
|
{
|
|
switchwin_T switchwin;
|
|
aco_save_T aco;
|
|
|
|
try_start();
|
|
switch (opt_type) {
|
|
case SREQ_WIN:
|
|
if (switch_win_noblock(&switchwin, (win_T *)from, win_find_tabpage((win_T *)from), true)
|
|
== FAIL) {
|
|
restore_win_noblock(&switchwin, true);
|
|
if (try_end(err)) {
|
|
return;
|
|
}
|
|
api_set_error(err,
|
|
kErrorTypeException,
|
|
"Problem while switching windows");
|
|
return;
|
|
}
|
|
set_option_value_err(key, numval, stringval, opt_flags, err);
|
|
restore_win_noblock(&switchwin, true);
|
|
break;
|
|
case SREQ_BUF:
|
|
aucmd_prepbuf(&aco, (buf_T *)from);
|
|
set_option_value_err(key, numval, stringval, opt_flags, err);
|
|
aucmd_restbuf(&aco);
|
|
break;
|
|
case SREQ_GLOBAL:
|
|
set_option_value_err(key, numval, stringval, opt_flags, err);
|
|
break;
|
|
}
|
|
|
|
if (ERROR_SET(err)) {
|
|
return;
|
|
}
|
|
|
|
try_end(err);
|
|
}
|
|
|
|
|
|
static void set_option_value_err(char *key, int numval, char *stringval, int opt_flags, Error *err)
|
|
{
|
|
char *errmsg;
|
|
|
|
if ((errmsg = set_option_value(key, numval, stringval, opt_flags))) {
|
|
if (try_end(err)) {
|
|
return;
|
|
}
|
|
|
|
api_set_error(err, kErrorTypeException, "%s", errmsg);
|
|
}
|
|
}
|
|
|
|
void api_set_error(Error *err, ErrorType errType, const char *format, ...)
|
|
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PRINTF(3, 4)
|
|
{
|
|
assert(kErrorTypeNone != errType);
|
|
va_list args1;
|
|
va_list args2;
|
|
va_start(args1, format);
|
|
va_copy(args2, args1);
|
|
int len = vsnprintf(NULL, 0, format, args1);
|
|
va_end(args1);
|
|
assert(len >= 0);
|
|
// Limit error message to 1 MB.
|
|
size_t bufsize = MIN((size_t)len + 1, 1024 * 1024);
|
|
err->msg = xmalloc(bufsize);
|
|
vsnprintf(err->msg, bufsize, format, args2);
|
|
va_end(args2);
|
|
|
|
err->type = errType;
|
|
}
|
|
|
|
/// Get an array containing dictionaries describing mappings
|
|
/// based on mode and buffer id
|
|
///
|
|
/// @param mode The abbreviation for the mode
|
|
/// @param buf The buffer to get the mapping array. NULL for global
|
|
/// @param from_lua Whether it is called from internal lua api.
|
|
/// @returns Array of maparg()-like dictionaries describing mappings
|
|
ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf, bool from_lua)
|
|
{
|
|
Array mappings = ARRAY_DICT_INIT;
|
|
dict_T *const dict = tv_dict_alloc();
|
|
|
|
// Convert the string mode to the integer mode
|
|
// that is stored within each mapblock
|
|
char_u *p = (char_u *)mode.data;
|
|
int int_mode = get_map_mode(&p, 0);
|
|
|
|
// Determine the desired buffer value
|
|
long buffer_value = (buf == NULL) ? 0 : buf->handle;
|
|
|
|
for (int i = 0; i < MAX_MAPHASH; i++) {
|
|
for (const mapblock_T *current_maphash = get_maphash(i, buf);
|
|
current_maphash;
|
|
current_maphash = current_maphash->m_next) {
|
|
// Check for correct mode
|
|
if (int_mode & current_maphash->m_mode) {
|
|
mapblock_fill_dict(dict, current_maphash, buffer_value, false);
|
|
Object api_dict = vim_to_object((typval_T[]) { { .v_type = VAR_DICT,
|
|
.vval.v_dict = dict } });
|
|
if (from_lua) {
|
|
Dictionary d = api_dict.data.dictionary;
|
|
for (size_t j = 0; j < d.size; j++) {
|
|
if (strequal("callback", d.items[j].key.data)) {
|
|
d.items[j].value.type = kObjectTypeLuaRef;
|
|
d.items[j].value.data.luaref = api_new_luaref((LuaRef)d.items[j].value.data.integer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
ADD(mappings, api_dict);
|
|
tv_dict_clear(dict);
|
|
}
|
|
}
|
|
}
|
|
tv_dict_free(dict);
|
|
|
|
return mappings;
|
|
}
|
|
|
|
/// Gets the line and column of an extmark.
|
|
///
|
|
/// Extmarks may be queried by position, name or even special names
|
|
/// in the future such as "cursor".
|
|
///
|
|
/// @param[out] lnum extmark line
|
|
/// @param[out] colnr extmark column
|
|
///
|
|
/// @return true if the extmark was found, else false
|
|
bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int
|
|
*row, colnr_T *col, Error *err)
|
|
{
|
|
// Check if it is mark id
|
|
if (obj.type == kObjectTypeInteger) {
|
|
Integer id = obj.data.integer;
|
|
if (id == 0) {
|
|
*row = 0;
|
|
*col = 0;
|
|
return true;
|
|
} else if (id == -1) {
|
|
*row = MAXLNUM;
|
|
*col = MAXCOL;
|
|
return true;
|
|
} else if (id < 0) {
|
|
api_set_error(err, kErrorTypeValidation, "Mark id must be positive");
|
|
return false;
|
|
}
|
|
|
|
ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id);
|
|
if (extmark.row >= 0) {
|
|
*row = extmark.row;
|
|
*col = extmark.col;
|
|
return true;
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation, "No mark with requested id");
|
|
return false;
|
|
}
|
|
|
|
// Check if it is a position
|
|
} else if (obj.type == kObjectTypeArray) {
|
|
Array pos = obj.data.array;
|
|
if (pos.size != 2
|
|
|| pos.items[0].type != kObjectTypeInteger
|
|
|| pos.items[1].type != kObjectTypeInteger) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Position must have 2 integer elements");
|
|
return false;
|
|
}
|
|
Integer pos_row = pos.items[0].data.integer;
|
|
Integer pos_col = pos.items[1].data.integer;
|
|
*row = (int)(pos_row >= 0 ? pos_row : MAXLNUM);
|
|
*col = (colnr_T)(pos_col >= 0 ? pos_col : MAXCOL);
|
|
return true;
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Position must be a mark id Integer or position Array");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
VirtText parse_virt_text(Array chunks, Error *err, int *width)
|
|
{
|
|
VirtText virt_text = KV_INITIAL_VALUE;
|
|
int w = 0;
|
|
for (size_t i = 0; i < chunks.size; i++) {
|
|
if (chunks.items[i].type != kObjectTypeArray) {
|
|
api_set_error(err, kErrorTypeValidation, "Chunk is not an array");
|
|
goto free_exit;
|
|
}
|
|
Array chunk = chunks.items[i].data.array;
|
|
if (chunk.size == 0 || chunk.size > 2
|
|
|| chunk.items[0].type != kObjectTypeString) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Chunk is not an array with one or two strings");
|
|
goto free_exit;
|
|
}
|
|
|
|
String str = chunk.items[0].data.string;
|
|
|
|
int hl_id = 0;
|
|
if (chunk.size == 2) {
|
|
Object hl = chunk.items[1];
|
|
if (hl.type == kObjectTypeArray) {
|
|
Array arr = hl.data.array;
|
|
for (size_t j = 0; j < arr.size; j++) {
|
|
hl_id = object_to_hl_id(arr.items[j], "virt_text highlight", err);
|
|
if (ERROR_SET(err)) {
|
|
goto free_exit;
|
|
}
|
|
if (j < arr.size-1) {
|
|
kv_push(virt_text, ((VirtTextChunk){ .text = NULL,
|
|
.hl_id = hl_id }));
|
|
}
|
|
}
|
|
} else {
|
|
hl_id = object_to_hl_id(hl, "virt_text highlight", err);
|
|
if (ERROR_SET(err)) {
|
|
goto free_exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
char *text = transstr(str.size > 0 ? str.data : "", false); // allocates
|
|
w += (int)mb_string2cells((char_u *)text);
|
|
|
|
kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id }));
|
|
}
|
|
|
|
*width = w;
|
|
return virt_text;
|
|
|
|
free_exit:
|
|
clear_virttext(&virt_text);
|
|
return virt_text;
|
|
}
|
|
|
|
/// Force obj to bool.
|
|
/// If it fails, returns false and sets err
|
|
/// @param obj The object to coerce to a boolean
|
|
/// @param what The name of the object, used for error message
|
|
/// @param nil_value What to return if the type is nil.
|
|
/// @param err Set if there was an error in converting to a bool
|
|
bool api_object_to_bool(Object obj, const char *what, bool nil_value, Error *err)
|
|
{
|
|
if (obj.type == kObjectTypeBoolean) {
|
|
return obj.data.boolean;
|
|
} else if (obj.type == kObjectTypeInteger) {
|
|
return obj.data.integer; // C semantics: non-zero int is true
|
|
} 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);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int object_to_hl_id(Object obj, const char *what, Error *err)
|
|
{
|
|
if (obj.type == kObjectTypeString) {
|
|
String str = obj.data.string;
|
|
return str.size ? syn_check_group(str.data, str.size) : 0;
|
|
} else if (obj.type == kObjectTypeInteger) {
|
|
return MAX((int)obj.data.integer, 0);
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"%s is not a valid highlight", what);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
HlMessage parse_hl_msg(Array chunks, Error *err)
|
|
{
|
|
HlMessage hl_msg = KV_INITIAL_VALUE;
|
|
for (size_t i = 0; i < chunks.size; i++) {
|
|
if (chunks.items[i].type != kObjectTypeArray) {
|
|
api_set_error(err, kErrorTypeValidation, "Chunk is not an array");
|
|
goto free_exit;
|
|
}
|
|
Array chunk = chunks.items[i].data.array;
|
|
if (chunk.size == 0 || chunk.size > 2
|
|
|| chunk.items[0].type != kObjectTypeString
|
|
|| (chunk.size == 2 && chunk.items[1].type != kObjectTypeString)) {
|
|
api_set_error(err, kErrorTypeValidation,
|
|
"Chunk is not an array with one or two strings");
|
|
goto free_exit;
|
|
}
|
|
|
|
String str = copy_string(chunk.items[0].data.string);
|
|
|
|
int attr = 0;
|
|
if (chunk.size == 2) {
|
|
String hl = chunk.items[1].data.string;
|
|
if (hl.size > 0) {
|
|
// TODO(bfredl): use object_to_hl_id and allow integer
|
|
int hl_id = syn_check_group(hl.data, hl.size);
|
|
attr = hl_id > 0 ? syn_id2attr(hl_id) : 0;
|
|
}
|
|
}
|
|
kv_push(hl_msg, ((HlMessageChunk){ .text = str, .attr = attr }));
|
|
}
|
|
|
|
return hl_msg;
|
|
|
|
free_exit:
|
|
clear_hl_msg(&hl_msg);
|
|
return hl_msg;
|
|
}
|
|
|
|
bool api_dict_to_keydict(void *rv, field_hash hashy, Dictionary dict, Error *err)
|
|
{
|
|
for (size_t i = 0; i < dict.size; i++) {
|
|
String k = dict.items[i].key;
|
|
Object *field = hashy(rv, k.data, k.size);
|
|
if (!field) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid key: '%.*s'", (int)k.size, k.data);
|
|
return false;
|
|
}
|
|
|
|
*field = dict.items[i].value;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void api_free_keydict(void *dict, KeySetLink *table)
|
|
{
|
|
for (size_t i = 0; table[i].str; i++) {
|
|
api_free_object(*(Object *)((char *)dict + table[i].ptr_off));
|
|
}
|
|
}
|
|
|
|
/// Set a named mark
|
|
/// buffer and mark name must be validated already
|
|
/// @param buffer Buffer to set the mark on
|
|
/// @param name Mark name
|
|
/// @param line Line number
|
|
/// @param col Column/row number
|
|
/// @return true if the mark was set, else false
|
|
bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err)
|
|
{
|
|
buf = buf == NULL ? curbuf : buf;
|
|
// If line == 0 the marks is being deleted
|
|
bool res = false;
|
|
bool deleting = false;
|
|
if (line == 0) {
|
|
col = 0;
|
|
deleting = true;
|
|
} else {
|
|
if (col > MAXCOL) {
|
|
api_set_error(err, kErrorTypeValidation, "Column value outside range");
|
|
return res;
|
|
}
|
|
if (line < 1 || line > buf->b_ml.ml_line_count) {
|
|
api_set_error(err, kErrorTypeValidation, "Line value outside range");
|
|
return res;
|
|
}
|
|
}
|
|
pos_T pos = { line, (int)col, (int)col };
|
|
res = setmark_pos(*name.data, &pos, buf->handle);
|
|
if (!res) {
|
|
if (deleting) {
|
|
api_set_error(err, kErrorTypeException,
|
|
"Failed to delete named mark: %c", *name.data);
|
|
} else {
|
|
api_set_error(err, kErrorTypeException,
|
|
"Failed to set named mark: %c", *name.data);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/// Get default statusline highlight for window
|
|
const char *get_default_stl_hl(win_T *wp)
|
|
{
|
|
if (wp == NULL) {
|
|
return "TabLineFill";
|
|
} else if (wp == curwin) {
|
|
return "StatusLine";
|
|
} else {
|
|
return "StatusLineNC";
|
|
}
|
|
}
|
|
|
|
void create_user_command(String name, Object command, Dict(user_command) *opts, int flags,
|
|
Error *err)
|
|
{
|
|
uint32_t argt = 0;
|
|
long def = -1;
|
|
cmd_addr_T addr_type_arg = ADDR_NONE;
|
|
int compl = EXPAND_NOTHING;
|
|
char *compl_arg = NULL;
|
|
char *rep = NULL;
|
|
LuaRef luaref = LUA_NOREF;
|
|
LuaRef compl_luaref = LUA_NOREF;
|
|
|
|
if (!uc_validate_name(name.data)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid command name");
|
|
goto err;
|
|
}
|
|
|
|
if (mb_islower(name.data[0])) {
|
|
api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter");
|
|
goto err;
|
|
}
|
|
|
|
if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) {
|
|
api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->nargs.type == kObjectTypeInteger) {
|
|
switch (opts->nargs.data.integer) {
|
|
case 0:
|
|
// Default value, nothing to do
|
|
break;
|
|
case 1:
|
|
argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
} else if (opts->nargs.type == kObjectTypeString) {
|
|
if (opts->nargs.data.string.size > 1) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
switch (opts->nargs.data.string.data[0]) {
|
|
case '*':
|
|
argt |= EX_EXTRA;
|
|
break;
|
|
case '?':
|
|
argt |= EX_EXTRA | EX_NOSPC;
|
|
break;
|
|
case '+':
|
|
argt |= EX_EXTRA | EX_NEEDARG;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
} else if (HAS_KEY(opts->nargs)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
if (HAS_KEY(opts->complete) && !argt) {
|
|
api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->range.type == kObjectTypeBoolean) {
|
|
if (opts->range.data.boolean) {
|
|
argt |= EX_RANGE;
|
|
addr_type_arg = ADDR_LINES;
|
|
}
|
|
} else if (opts->range.type == kObjectTypeString) {
|
|
if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) {
|
|
argt |= EX_RANGE | EX_DFLALL;
|
|
addr_type_arg = ADDR_LINES;
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
|
|
goto err;
|
|
}
|
|
} else if (opts->range.type == kObjectTypeInteger) {
|
|
argt |= EX_RANGE | EX_ZEROR;
|
|
def = opts->range.data.integer;
|
|
addr_type_arg = ADDR_LINES;
|
|
} else if (HAS_KEY(opts->range)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->count.type == kObjectTypeBoolean) {
|
|
if (opts->count.data.boolean) {
|
|
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
|
|
addr_type_arg = ADDR_OTHER;
|
|
def = 0;
|
|
}
|
|
} else if (opts->count.type == kObjectTypeInteger) {
|
|
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
|
|
addr_type_arg = ADDR_OTHER;
|
|
def = opts->count.data.integer;
|
|
} else if (HAS_KEY(opts->count)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->addr.type == kObjectTypeString) {
|
|
if (parse_addr_type_arg((char_u *)opts->addr.data.string.data, (int)opts->addr.data.string.size,
|
|
&addr_type_arg) != OK) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
|
|
goto err;
|
|
}
|
|
|
|
if (addr_type_arg != ADDR_LINES) {
|
|
argt |= EX_ZEROR;
|
|
}
|
|
} else if (HAS_KEY(opts->addr)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->bang, "bang", false, err)) {
|
|
argt |= EX_BANG;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->bar, "bar", false, err)) {
|
|
argt |= EX_TRLBAR;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
|
|
if (api_object_to_bool(opts->register_, "register", false, err)) {
|
|
argt |= EX_REGSTR;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) {
|
|
argt |= EX_KEEPSCRIPT;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
bool force = api_object_to_bool(opts->force, "force", true, err);
|
|
if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (opts->complete.type == kObjectTypeLuaRef) {
|
|
compl = EXPAND_USER_LUA;
|
|
compl_luaref = api_new_luaref(opts->complete.data.luaref);
|
|
} else if (opts->complete.type == kObjectTypeString) {
|
|
if (parse_compl_arg((char_u *)opts->complete.data.string.data,
|
|
(int)opts->complete.data.string.size, &compl, &argt,
|
|
(char_u **)&compl_arg) != OK) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
|
|
goto err;
|
|
}
|
|
} else if (HAS_KEY(opts->complete)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
|
|
goto err;
|
|
}
|
|
|
|
switch (command.type) {
|
|
case kObjectTypeLuaRef:
|
|
luaref = api_new_luaref(command.data.luaref);
|
|
if (opts->desc.type == kObjectTypeString) {
|
|
rep = opts->desc.data.string.data;
|
|
} else {
|
|
snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref);
|
|
rep = (char *)IObuff;
|
|
}
|
|
break;
|
|
case kObjectTypeString:
|
|
rep = command.data.string.data;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function");
|
|
goto err;
|
|
}
|
|
|
|
if (uc_add_command((char_u *)name.data, name.size, (char_u *)rep, argt, def, flags,
|
|
compl, (char_u *)compl_arg, compl_luaref, addr_type_arg, luaref,
|
|
force) != OK) {
|
|
api_set_error(err, kErrorTypeException, "Failed to create user command");
|
|
goto err;
|
|
}
|
|
|
|
return;
|
|
|
|
err:
|
|
NLUA_CLEAR_REF(luaref);
|
|
NLUA_CLEAR_REF(compl_luaref);
|
|
}
|
|
|
|
int find_sid(uint64_t channel_id)
|
|
{
|
|
switch (channel_id) {
|
|
case VIML_INTERNAL_CALL:
|
|
// TODO(autocmd): Figure out what this should be
|
|
// return SID_API_CLIENT;
|
|
case LUA_INTERNAL_CALL:
|
|
return SID_LUA;
|
|
default:
|
|
return SID_API_CLIENT;
|
|
}
|
|
}
|
|
|
|
/// Sets sctx for API calls.
|
|
///
|
|
/// @param channel_id api clients id. Used to determine if it's a internal
|
|
/// call or a rpc call.
|
|
/// @return returns previous value of current_sctx. To be used
|
|
/// to be used for restoring sctx to previous state.
|
|
sctx_T api_set_sctx(uint64_t channel_id)
|
|
{
|
|
sctx_T old_current_sctx = current_sctx;
|
|
if (channel_id != VIML_INTERNAL_CALL) {
|
|
current_sctx.sc_sid =
|
|
channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT;
|
|
current_sctx.sc_lnum = 0;
|
|
}
|
|
return old_current_sctx;
|
|
}
|
|
|
|
// adapted from sign.c:sign_define_init_text.
|
|
// TODO(lewis6991): Consider merging
|
|
int init_sign_text(char_u **sign_text, char_u *text)
|
|
{
|
|
char_u *s;
|
|
|
|
char_u *endp = text + (int)STRLEN(text);
|
|
|
|
// Count cells and check for non-printable chars
|
|
int cells = 0;
|
|
for (s = text; s < endp; s += utfc_ptr2len(s)) {
|
|
if (!vim_isprintc(utf_ptr2char(s))) {
|
|
break;
|
|
}
|
|
cells += utf_ptr2cells(s);
|
|
}
|
|
// Currently must be empty, one or two display cells
|
|
if (s != endp || cells > 2) {
|
|
return FAIL;
|
|
}
|
|
if (cells < 1) {
|
|
return OK;
|
|
}
|
|
|
|
// Allocate one byte more if we need to pad up
|
|
// with a space.
|
|
size_t len = (size_t)(endp - text + ((cells == 1) ? 1 : 0));
|
|
*sign_text = vim_strnsave(text, len);
|
|
|
|
if (cells == 1) {
|
|
STRCPY(*sign_text + len - 1, " ");
|
|
}
|
|
|
|
return OK;
|
|
}
|