feat(api): nvim_set_option_value(operation=...) #39849

Problem:
`nvim_set_option_value` cannot "update" options similar to `:set opt=`,
`:set opt+=`, etc. The Lua impls of "vim.opt" / "vim.o" have incomplete,
bespoke reimplementations of those operations.

ref #38420

Solution:
- Add `operation` param to `nvim_set_option_value`, which may be "set",
  "append", "prepend", or "remove".
- Use this feature to implement `vim.opt` / `vim.o`.
This commit is contained in:
Kyle
2026-06-28 12:20:56 -05:00
committed by GitHub
parent a5aa62e37b
commit f34ee3da80
12 changed files with 598 additions and 243 deletions

View File

@@ -170,6 +170,8 @@ typedef struct {
Buffer buf;
Tabpage tab;
String filetype;
String operation;
Boolean dry_run;
} Dict(option);
typedef struct {

View File

@@ -13,11 +13,14 @@
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/globals.h"
#include "nvim/lua/executor.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
@@ -26,7 +29,8 @@
static int validate_option_value_args(Dict(option) *opts, char *name, bool allow_tab,
OptIndex *opt_idxp, int *opt_flags, OptScope *scope,
void **from, char **filetype, Error *err)
void **from, char **filetype, set_op_T *operation,
bool *dry_run, Error *err)
{
#define HAS_KEY_X(d, v) HAS_KEY(d, option, v)
// Validate incompatible argument combinations first, then resolve handles and scope.
@@ -105,6 +109,34 @@ static int validate_option_value_args(Dict(option) *opts, char *name, bool allow
return FAIL;
}
if (operation != NULL && HAS_KEY_X(opts, operation)) {
if (strequal(opts->operation.data, "set")) {
*operation = OP_NONE;
} else if (strequal(opts->operation.data, "append")) {
*operation = OP_ADDING;
} else if (strequal(opts->operation.data, "prepend")) {
*operation = OP_PREPENDING;
} else if (strequal(opts->operation.data, "remove")) {
*operation = OP_REMOVING;
} else {
VALIDATE_EXP(false, "operation", "'set', 'append', 'prepend', or 'remove'", NULL, {
return FAIL;
});
}
VALIDATE_CON(*operation == OP_NONE || option_has_type(*opt_idxp,
kOptValTypeString)
|| option_has_type(*opt_idxp, kOptValTypeNumber),
opts->operation.data,
"boolean options", {
return FAIL;
});
}
if (dry_run != NULL && HAS_KEY_X(opts, dry_run)) {
*dry_run = opts->dry_run;
}
// Reject keys whose scope the option doesn't support.
VALIDATE_CON(!HAS_KEY_X(opts, tab) || option_has_scope(*opt_idxp, kOptScopeTab),
"tab", name, { return FAIL; });
@@ -235,7 +267,7 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err)
char *filetype = NULL;
if (!validate_option_value_args(opts, name.data, true, &opt_idx, &opt_flags, &scope, &from,
&filetype, err)) {
&filetype, NULL, NULL, err)) {
return (Object)OBJECT_INIT;
}
@@ -288,6 +320,11 @@ err:
/// @param value New option value
/// @param opts Optional parameters
/// - buf: Buffer number. Used for setting buffer local option.
/// - dry_run: (`boolean?`, default: false) If true, then the
/// option value won't be set.
/// - operation: One of "set", "append", "prepend", or "remove".
/// Corresponds to |:set=|, |:set+=|, |:set^=|, and |:set-=|.
/// Default is "set".
/// - scope: One of "global" or "local". Analogous to
/// |:setglobal| and |:setlocal|, respectively.
/// - tab: |tab-ID| for tab-local options (currently only 'cmdheight'). Tabpage 0
@@ -295,17 +332,20 @@ err:
/// effect when it is switched-to.
/// - win: |window-ID|. Used for setting window local option.
/// @param[out] err Error details, if any
void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(option) *opts,
Error *err)
/// @return Option value
Object nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(option) *opts,
Arena *arena, Error *err)
FUNC_API_SINCE(9)
{
OptIndex opt_idx = 0;
int opt_flags = 0;
OptScope scope = kOptScopeGlobal;
set_op_T operation = OP_NONE;
void *to = NULL;
bool dry_run = false;
if (!validate_option_value_args(opts, name.data, true, &opt_idx, &opt_flags, &scope, &to, NULL,
err)) {
return;
&operation, &dry_run, err)) {
return NIL;
}
// If:
@@ -320,20 +360,99 @@ void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(
}
}
// Convert the incoming Lua object (which could be a table) into the proper
// OptVal type (string even for list/dict style options)
Error lua_err = ERROR_INIT;
MAXSIZE_TEMP_ARRAY(args, 3);
ADD_C(args, STRING_OBJ(name));
ADD_C(args, value);
ADD_C(args, CSTR_AS_OBJ(set_op_get_name(operation)));
Object vim_val =
NLUA_EXEC_STATIC("return require('vim._core.options').convert_value_to_vim(...)",
args, kRetObject, NULL, &lua_err);
VALIDATE(!ERROR_SET(&lua_err), "%s", lua_err.msg, {
api_clear_error(&lua_err);
return NIL;
});
bool error = false;
OptVal optval = object_as_optval(value, &error);
OptVal optval_right = object_as_optval(vim_val, &error);
// Handle invalid option value type.
// Don't use `name` in the error message here, because `name` can be any String.
// No need to check if value type actually matches the types for the option, as set_option_value()
// already handles that.
VALIDATE_EXP(!error, "value", "valid option type", api_typename(value.type), {
return;
return NIL;
});
WITH_SCRIPT_CONTEXT(channel_id, {
set_option_value_for(name.data, opt_idx, optval, opt_flags, scope, to, err);
});
OptVal merged_val = NIL_OPTVAL;
const char *errmsg = NULL;
vimoption_T *option = get_option(opt_idx);
// Need to use varp specific to buf/win to ensure that merges are handled
// correctly when the supplied buf/win are different than curbuf/curwin.
buf_T *buf = scope == kOptScopeBuf ? to : curbuf;
win_T *win = scope == kOptScopeWin ? to : curwin;
void *varp = get_varp_from(option, buf, win);
char *argp = NULL;
switch (optval_right.type) {
case kOptValTypeNil:
break;
case kOptValTypeString: {
char *optval_escaped = escape_option_str_cmdline(optval_right.data.string.data);
// We need a leading equal sign because get_option_newval is used for
// cmdline stuff and expects an =
argp = arena_printf(arena, "=%s", optval_escaped).data;
XFREE_CLEAR(optval_escaped);
break;
}
case kOptValTypeNumber:
argp = arena_printf(arena, "=%" PRId64, optval_right.data.number).data;
break;
case kOptValTypeBoolean:
merged_val = optval_right;
break;
}
optval_free(optval_right);
if (optval_right.type == kOptValTypeNumber || optval_right.type == kOptValTypeString) {
OptVal oldval = optval_from_varp(opt_idx, varp);
merged_val = get_option_newval(opt_idx, opt_flags, PREFIX_NONE, &argp, 0, operation,
option->flags, varp, &oldval, NULL, 0, &errmsg);
VALIDATE(errmsg == NULL, "%s", errmsg, {
return NIL;
});
}
if (!dry_run) {
WITH_SCRIPT_CONTEXT(channel_id, {
set_option_value_for(name.data, opt_idx, merged_val, opt_flags, scope, to, err);
});
}
if (merged_val.type == kOptValTypeString) {
// Convert the return type to lua for string/list/map style option
lua_err = ERROR_INIT;
MAXSIZE_TEMP_ARRAY(lua_args, 2);
ADD_C(lua_args, STRING_OBJ(name));
ADD_C(lua_args, STRING_OBJ(merged_val.data.string));
Object lua_val =
NLUA_EXEC_STATIC("return require('vim._core.options').convert_value_to_lua(...)",
lua_args, kRetObject, arena, &lua_err);
optval_free(merged_val);
VALIDATE(!ERROR_SET(&lua_err), "%s", lua_err.msg, {
api_clear_error(&lua_err);
return NIL;
});
return lua_val;
}
return optval_as_object(merged_val);
}
/// Gets the option information for all options.
@@ -393,7 +512,7 @@ DictAs(get_option_info) nvim_get_option_info2(String name, Dict(option) *opts, A
void *from = NULL;
// TODO(justinmk): support tab-local option.
if (!validate_option_value_args(opts, name.data, false, &opt_idx, &opt_flags, &scope, &from, NULL,
err)) {
NULL, NULL, err)) {
return (Dict)ARRAY_DICT_INIT;
}

View File

@@ -151,13 +151,6 @@ static char *p_vsts_nopaste;
#define OPTION_COUNT ARRAY_SIZE(options)
/// :set boolean option prefix
typedef enum {
PREFIX_NO = 0, ///< "no" prefix
PREFIX_NONE, ///< no prefix
PREFIX_INV, ///< "inv" prefix
} set_prefix_T;
#include "option.c.generated.h"
// options[] is initialized in options.generated.h.
@@ -1100,14 +1093,15 @@ static void stropt_remove_dupflags(char *newval, uint32_t flags)
/// Get the string value specified for a ":set" command. The following set options are supported:
/// set {opt}={val}
/// set {opt}:{val}
static char *stropt_get_newval(int nextchar, OptIndex opt_idx, char **argp, void *varp,
const char *origval, set_op_T *op_arg, uint32_t flags)
static char *stropt_get_newval(OptIndex opt_idx, char **argp, void *varp, const char *origval,
set_op_T *op_arg)
{
char *arg = *argp;
set_op_T op = *op_arg;
char *save_arg = NULL;
char *newval;
const char *s = NULL;
uint32_t flags = options[opt_idx].flags;
arg++; // jump to after the '=' or ':'
@@ -1326,18 +1320,29 @@ const char *find_option_end(const char *arg, OptIndex *opt_idxp)
/// Get new option value from argp. Allocated OptVal must be freed by caller.
/// Can unset local value of an option when ":set {option}<" is used.
static OptVal get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, char **argp,
int nextchar, set_op_T op, uint32_t flags, void *varp, char *errbuf,
const size_t errbuflen, const char **errmsg)
OptVal get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, char **argp,
int nextchar, set_op_T op, uint32_t flags, void *varp,
OptVal *oldval_override, char *errbuf, const size_t errbuflen,
const char **errmsg)
FUNC_ATTR_WARN_UNUSED_RESULT
{
assert(varp != NULL);
vimoption_T *opt = &options[opt_idx];
char *arg = *argp;
// When setting the local value of a global option, the old value may be the global value.
const bool oldval_is_global = option_is_global_local(opt_idx) && (opt_flags & OPT_LOCAL);
OptVal oldval = optval_from_varp(opt_idx, oldval_is_global ? get_varp(opt) : varp);
OptVal oldval;
if (oldval_override != NULL) {
// Allow overriding the oldval. This is needed to handle the case where
// options for buffers/windows other than curbuf/curwin are updated. It can
// also support merging arbitrary values if necessary down the road.
oldval = *oldval_override;
} else {
// When setting the local value of a global option, the old value may be the global value.
const bool oldval_is_global = option_is_global_local(opt_idx) && (opt_flags & OPT_LOCAL);
oldval = optval_from_varp(opt_idx, oldval_is_global ? get_varp(opt) : varp);
}
OptVal newval = NIL_OPTVAL;
if (nextchar == '&') {
@@ -1434,8 +1439,7 @@ static OptVal get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T pr
case kOptValTypeString: {
const char *oldval_str = oldval.data.string.data;
// Get the new value for the option
const char *newval_str = stropt_get_newval(nextchar, opt_idx, argp, varp, oldval_str, &op,
flags);
const char *newval_str = stropt_get_newval(opt_idx, argp, varp, oldval_str, &op);
newval = CSTR_AS_OPTVAL(newval_str);
break;
}
@@ -1555,7 +1559,7 @@ static void do_one_set_option(int opt_flags, char **argp, bool *did_show, char *
}
OptVal newval = get_option_newval(opt_idx, opt_flags, prefix, argp, nextchar, op, flags, varp,
errbuf, errbuflen, errmsg);
NULL, errbuf, errbuflen, errmsg);
if (newval.type == kOptValTypeNil || *errmsg != NULL) {
return;
@@ -6072,7 +6076,7 @@ int ExpandSettings(expand_T *xp, regmatch_T *regmatch, char *fuzzystr, int *numM
/// Escape an option value that can be used on the command-line with :set.
/// Caller needs to free the returned string, unless NULL is returned.
static char *escape_option_str_cmdline(char *var)
char *escape_option_str_cmdline(char *var)
{
// A backslash is required before some characters. This is the reverse of
// what happens in do_set().

View File

@@ -33,6 +33,13 @@ typedef enum {
OPT_SKIPRTP = 0x80, ///< "skiprtp" in 'sessionoptions'
} OptionSetFlags;
/// :set boolean option prefix
typedef enum {
PREFIX_NO = 0, ///< "no" prefix
PREFIX_NONE, ///< no prefix
PREFIX_INV, ///< "inv" prefix
} set_prefix_T;
/// Get name of OptValType as a string.
static inline const char *optval_type_get_name(const OptValType type)
{
@@ -49,6 +56,22 @@ static inline const char *optval_type_get_name(const OptValType type)
UNREACHABLE;
}
/// Get name of set_op_T as a string
static inline const char *set_op_get_name(const set_op_T op)
{
switch (op) {
case OP_NONE:
return "set";
case OP_ADDING:
return "append";
case OP_PREPENDING:
return "prepend";
case OP_REMOVING:
return "remove";
}
UNREACHABLE;
}
// OptVal helper macros.
#define NIL_OPTVAL ((OptVal) { .type = kOptValTypeNil })
#define BOOLEAN_OPTVAL(b) ((OptVal) { .type = kOptValTypeBoolean, .data.boolean = b })