refactor(options): memory management

Problem:
Numerous callers have to manually check optval ownership (i.e. whether,
and how, to release) via `is_callback_option`, `option_is_global_local`,
etc. This is fragile, hard to use correctly; and if we introduce another
optval variant in the future, we'll have to redo all of these careful
checks and boilerplate again.

Solution:
Provide a unified system and use it everywhere:

    optval_free_owned
    optval_is_owned
    optval_own
This commit is contained in:
Justin M. Keyes
2026-07-30 21:49:32 +02:00
parent 726d1a92d2
commit 3e01dcddbe
11 changed files with 208 additions and 100 deletions

View File

@@ -395,7 +395,7 @@ Note: In the future more global options can be made |global-local|. Using
*option-value-function*
Some options ('completefunc', 'findfunc', 'omnifunc', 'operatorfunc',
'quickfixtextfunc', 'tagfunc' and 'thesaurusfunc') can be set to a function
name, a |Funcref|, a |lambda|, or (from Lua) a Lua function.
name, a |Funcref|, a |lambda|, or a Lua function.
Examples:
>
set opfunc=MyOpFunc
@@ -428,6 +428,8 @@ The value read back from the option reflects how it was set:
- A |script-local| ("s:") name is expanded to `<SNR>` form.
- A Lua function is returned as itself from Lua, or as a `"<Lua …>"` hint
from Vimscript/RPC.
- Note: Lua functions cannot be serialized, so it cannot be saved/restored
by |:mksession| |:mkview| |:mkvimrc|.
Calling a function in an expr option *expr-option-function*
@@ -5488,7 +5490,7 @@ A jump table for the options with a short description can be found at |Q_op|.
localoptions options and mappings local to a window or buffer (not
global values for local options)
options all options and mappings (also global values for local
options)
options), except Lua functions |option-value-function|.
skiprtp exclude 'runtimepath' and 'packpath' from the options
resize size of the Vim window: 'lines' and 'columns'
sesdir the directory in which the session file is located
@@ -7307,7 +7309,8 @@ A jump table for the options with a short description can be found at |Q_op|.
folds manually created folds, opened/closed folds and local
fold options
options options and mappings local to a window or buffer (not
global values for local options)
global values for local options), except Lua functions
|option-value-function|.
localoptions same as "options"
slash |deprecated| Always enabled. Uses "/" in filenames.
unix |deprecated| Always enabled. Uses "\n" line endings.

View File

@@ -800,7 +800,7 @@ The output of ":mksession" is like ":mkvimrc", but additional commands are
added to the file. Which ones depends on the 'sessionoptions' option. The
resulting file, when executed with a ":source" command:
1. Restores global mappings and options, if 'sessionoptions' contains
"options". Script-local mappings will not be written.
"options". Lua functions, and script-local mappings, will not be written.
2. Restores global variables that start with an uppercase letter and contain
at least one lowercase letter, if 'sessionoptions' contains "globals".
3. Closes all windows in the current tabpage, except the current one; closes
@@ -872,6 +872,7 @@ The output of ":mkview" contains these items:
that are local to the current buffer and the current window are restored.
When storing the view as part of a session and "options" is in
'sessionoptions', global values for local options will be stored too.
Lua functions are not restored.
4. Restore folds when using manual folding and 'viewoptions' contains
"folds". Restore manually opened and closed folds.
5. The scroll position and the cursor position in the file. Doesn't work very

View File

@@ -5718,7 +5718,7 @@ vim.go.slm = vim.go.selectmode
--- localoptions options and mappings local to a window or buffer (not
--- global values for local options)
--- options all options and mappings (also global values for local
--- options)
--- options), except Lua functions `option-value-function`.
--- skiprtp exclude 'runtimepath' and 'packpath' from the options
--- resize size of the Vim window: 'lines' and 'columns'
--- sesdir the directory in which the session file is located
@@ -7885,7 +7885,8 @@ vim.go.vdir = vim.go.viewdir
--- folds manually created folds, opened/closed folds and local
--- fold options
--- options options and mappings local to a window or buffer (not
--- global values for local options)
--- global values for local options), except Lua functions
--- `option-value-function`.
--- localoptions same as "options"
--- slash `deprecated` Always enabled. Uses "/" in filenames.
--- unix `deprecated` Always enabled. Uses "\n" line endings.

View File

@@ -402,9 +402,10 @@ Object nvim_set_option_value(uint64_t channel_id, String name, Object value, Dic
optval_free(optval_right);
if (optval_right.type == kObjectTypeInteger || optval_right.type == kObjectTypeString) {
Object oldval = opt_from_varp(opt_idx, varp);
Object oldval = optval_own(option->flags, opt_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);
optval_free(oldval);
VALIDATE(errmsg == NULL, "%s", errmsg, {
return NIL;
});

View File

@@ -3443,6 +3443,7 @@ int eval_option(const char **const arg, typval_T *const rettv, const bool evalua
assert(value.type != kObjectTypeNil);
*rettv = opt_to_tv(value, true);
optval_free(value);
} else if (working && !is_tty_opt && is_option_hidden(opt_idx)) {
ret = FAIL;
}

View File

@@ -3260,7 +3260,7 @@ static Object opt_from_tv(typval_T *tv, OptIndex opt_idx, const char *option, bo
return value;
}
/// Converts an option value to typval.
/// Converts an option value to typval. Caller must tv_clear() the result.
///
/// @param[in] value Option value to convert.
/// @param numbool Whether to convert boolean values to number.
@@ -3297,7 +3297,7 @@ typval_T opt_to_tv(Object value, bool numbool)
break;
case kObjectTypeString:
rettv.v_type = VAR_STRING;
rettv.vval.v_string = value.data.string.data;
rettv.vval.v_string = value.data.string.data != NULL ? xstrdup(value.data.string.data) : NULL;
break;
case kObjectTypeLuaRef:
// Lua callback option (e.g. 'operatorfunc'): show a human-readable hint (same as `:map` does).

View File

@@ -316,8 +316,9 @@ static void set_init_expand_env(void)
p = option_expand(opt_idx, NULL);
}
if (p != NULL) {
set_option_varp(opt_idx, opt->var, CSTR_TO_OBJ(p), true);
change_option_default(opt_idx, CSTR_TO_OBJ(p));
Object ov = CSTR_TO_OBJ(p);
set_option_varp(opt_idx, opt->var, ov); // Copies `ov`.
change_option_default(opt_idx, ov); // Consumes `ov`.
}
}
}
@@ -442,7 +443,7 @@ void set_init_1(bool clean_arg)
/// @param opt_idx Option index in options[] table.
/// @param opt_flags Option flags (can be OPT_LOCAL, OPT_GLOBAL or a combination).
///
/// @return Default value of option for the scope specified in opt_flags.
/// @return Default value for the scope specified by opt_flags. Release with optval_free_read().
Object get_option_default(const OptIndex opt_idx, int opt_flags)
{
vimoption_T *opt = &options[opt_idx];
@@ -458,14 +459,18 @@ Object get_option_default(const OptIndex opt_idx, int opt_flags)
if ((opt_flags & OPT_LOCAL) && is_global_local_option) {
// Use unset local value instead of default value for local scope of global-local options.
return get_option_unset_value(opt_idx);
} else if (option_has_type(opt_idx, kObjectTypeString) && !(opt->flags & kOptFlagNoDefExp)) {
}
Object def_val = opt->def_val;
if (option_has_type(opt_idx, kObjectTypeString) && !(opt->flags & kOptFlagNoDefExp)) {
// For string options, expand environment variables and ~ since the default value was already
// expanded, only required when an environment variable was set later.
char *s = option_expand(opt_idx, opt->def_val.data.string.data);
return s == NULL ? opt->def_val : CSTR_AS_OBJ(s);
} else {
return opt->def_val;
char *s = option_expand(opt_idx, def_val.data.string.data);
if (s != NULL) {
def_val = CSTR_AS_OBJ(s);
}
}
return optval_is_owned(opt->flags) ? copy_object(def_val, NULL) : def_val;
}
/// Allocate the default values for all options by copying them from the stack.
@@ -497,6 +502,7 @@ static void set_option_default(const OptIndex opt_idx, int opt_flags)
bool both = (opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0;
Object def_val = get_option_default(opt_idx, opt_flags);
set_option_direct(opt_idx, def_val, opt_flags, current_sctx.sc_sid);
optval_free_read(options[opt_idx].flags, def_val);
if (opt_idx == kOptScroll) {
win_comp_scroll(curwin);
@@ -1337,25 +1343,13 @@ Object get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, c
vimoption_T *opt = &options[opt_idx];
char *arg = *argp;
Object 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 = opt_from_varp(opt_idx, oldval_is_global ? get_varp(opt) : varp);
}
Object newval = NIL;
if (nextchar == '&') {
// ":set opt&": Reset to default value.
// NOTE: Use OPT_GLOBAL instead of opt_flags to ensure we don't use the unset local value for
// global-local options when OPT_LOCAL is used.
return copy_object(get_option_default(opt_idx, OPT_GLOBAL), NULL);
return optval_own(flags, get_option_default(opt_idx, OPT_GLOBAL));
} else if (nextchar == '<') {
// ":set opt<": Reset to global value.
// ":setlocal opt<": Copy global value to local value.
@@ -1365,6 +1359,19 @@ Object get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, c
return get_option_value(opt_idx, OPT_GLOBAL);
}
// Computed lazily: the ":set opt&"/"opt<" resets above return without needing the old value.
Object 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_own(flags, opt_from_varp(opt_idx, oldval_is_global ? get_varp(opt) : varp));
}
switch (oldval.type) {
case kObjectTypeNil:
abort();
@@ -1454,6 +1461,10 @@ Object get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, c
abort();
}
// oldval (computed above, not overridden) is owned via optval_own(); an override is borrowed.
if (oldval_override == NULL) {
optval_free(oldval);
}
return newval;
}
@@ -2121,6 +2132,11 @@ void apply_optionset_autocmd_now(OptIndex opt_idx, int opt_flags, Object oldval,
}
apply_autocmds(EVENT_OPTIONSET, options[opt_idx].fullname, NULL, false, NULL);
reset_v_option_vars();
tv_clear(&oldval_tv);
tv_clear(&oldval_g_tv);
tv_clear(&oldval_l_tv);
tv_clear(&newval_tv);
}
/// For 'modified', the event is deferred.
@@ -2401,7 +2417,7 @@ static const char *did_set_lines_or_columns(optset_T *args)
// Changing the screen size is not allowed while updating the screen.
if (updating_screen) {
Object oldval = INTEGER_OBJ(args->os_oldval.data.integer);
set_option_varp(args->os_idx, args->os_varp, oldval, false);
set_option_varp(args->os_idx, args->os_varp, oldval);
} else if (full_screen) {
screen_resize((int)p_columns, (int)p_lines);
} else {
@@ -3348,6 +3364,14 @@ bool is_dict_option(OptIndex opt_idx)
return opt_dict_schema(opt_idx) != NULL;
}
/// Release an option value read from opt_from_varp() (and similar).
void optval_free_read(uint32_t flags, Object value)
{
if (optval_is_owned(flags)) {
optval_free(value);
}
}
/// Free an allocated option value.
void optval_free(Object o)
{
@@ -3431,9 +3455,41 @@ static bool is_callback_option(uint32_t flags)
return (flags & (kOptFlagFunc | kOptFlagExpr)) != 0;
}
/// Returns true if _reading_ the option value (opt_from_varp(), etc.) returns an owned Object
/// (release with optval_free_read()), or false if it borrows the stored value.
static bool optval_is_owned(uint32_t flags)
{
return is_callback_option(flags);
}
/// Ensures an option value is "owned", i.e. the caller decides when to free it.
Object optval_own(uint32_t flags, Object value)
{
return optval_is_owned(flags) ? value : copy_object(value, NULL);
}
/// Copies an option value for OptionSet/UI reporting. Lua function is reported as "<Lua N>".
static Object optval_snapshot(Object value)
{
if (value.type == kObjectTypeLuaRef) {
return CSTR_AS_OBJ(nlua_funcref_str(value.data.luaref, NULL));
}
return copy_object(value, NULL);
}
/// Reads the option value at `varp` and returns a snapshot (see optval_snapshot()).
static Object optval_snapshot_varp(OptIndex opt_idx, void *varp)
{
Object value = opt_from_varp(opt_idx, varp);
Object snapshot = optval_snapshot(value);
optval_free_read(options[opt_idx].flags, value);
return snapshot;
}
/// Converts a callback option's stored Callback to an option value Object.
/// Always OWNED (a copied name/expr string, or a fresh LuaRef): free with optval_free().
///
/// - Lua callback => `LuaRef` (owned by the caller)
/// - Lua callback => `LuaRef`
/// - named funcref => name
/// - partial => name
/// - expr option => expression string
@@ -3490,7 +3546,7 @@ static Callback opt_to_callback(Object value, bool is_expr)
return cb;
}
/// Creates Object from var pointer.
/// Creates Object from var pointer. Release with optval_free_read(), or promote with optval_own().
///
/// @param opt_idx Option index in options[] table.
/// @param[out] varp Pointer to option variable.
@@ -3523,29 +3579,19 @@ Object opt_from_varp(OptIndex opt_idx, void *varp)
}
}
/// Sets an option var pointer (the legacy/scalar value) from a structured value.
///
/// @param opt_idx Option index in options[] table.
/// @param[out] varp Pointer to option variable.
/// @param[in] value New option value.
/// @param free_oldval Free old value.
static void set_option_varp(OptIndex opt_idx, void *varp, Object value, bool free_oldval)
/// Stores an option value at its "varp", freeing the previously-stored value. `value` is copied
/// in (caller retains ownership).
static void set_option_varp(OptIndex opt_idx, void *varp, Object value)
FUNC_ATTR_NONNULL_ARG(2)
{
// Callback options (e.g. 'operatorfunc', 'foldexpr') store a Callback, not a scalar.
if (is_callback_option(options[opt_idx].flags)) {
Callback *cb = (Callback *)varp;
if (free_oldval) {
callback_free(cb);
}
callback_free(cb);
*cb = opt_to_callback(value, options[opt_idx].flags & kOptFlagExpr);
return;
}
if (free_oldval) {
optval_free(opt_from_varp(opt_idx, varp));
}
switch (value.type) {
case kObjectTypeUnset:
// Unset local value of a global-local boolean: store the kNone sentinel.
@@ -3562,7 +3608,8 @@ static void set_option_varp(OptIndex opt_idx, void *varp, Object value, bool fre
return;
case kObjectTypeString:
assert(option_has_type(opt_idx, kObjectTypeString));
*(char **)varp = value.data.string.data;
free_string_option(*(char **)varp);
*(char **)varp = copy_option_val(value.data.string.data);
return;
default:
abort();
@@ -3909,9 +3956,7 @@ Object get_option_value(OptIndex opt_idx, int opt_flags)
}
vimoption_T *opt = &options[opt_idx];
void *varp = get_varp_scope(opt, opt_flags);
return copy_object(opt_from_varp(opt_idx, varp), NULL);
return optval_own(opt->flags, opt_from_varp(opt_idx, get_varp_scope(opt, opt_flags)));
}
/// Return information for option at 'opt_idx'
@@ -3921,11 +3966,11 @@ vimoption_T *get_option(OptIndex opt_idx)
return &options[opt_idx];
}
/// Get option value that represents an unset local value for an option.
/// Get option value representing an unset local option. Release with optval_free_read().
/// TODO(famiu): Remove this once we have a dedicated Object type for unset local options.
///
/// @param opt_idx Option index in options[] table.
/// @param[in] varp Pointer to option variable.
///
/// @param opt_idx Option index in options[] table.
///
/// @return Option value equal to the unset value for the option.
static Object get_option_unset_value(OptIndex opt_idx)
@@ -3937,7 +3982,7 @@ static Object get_option_unset_value(OptIndex opt_idx)
if (option_is_global_local(opt_idx)) {
// String global-local options always use an empty string for the unset value.
if (option_has_type(opt_idx, kObjectTypeString)) {
return STATIC_CSTR_AS_OBJ("");
return optval_is_owned(opt->flags) ? STATIC_CSTR_TO_OBJ("") : STATIC_CSTR_AS_OBJ("");
}
switch (opt_idx) {
@@ -3977,7 +4022,10 @@ static bool is_option_local_value_unset(OptIndex opt_idx)
Object local_value = opt_from_varp(opt_idx, varp_local);
Object unset_local_value = get_option_unset_value(opt_idx);
return option_equal(local_value, unset_local_value);
bool is_unset = option_equal(local_value, unset_local_value);
optval_free_read(opt->flags, local_value);
optval_free_read(opt->flags, unset_local_value);
return is_unset;
}
/// Handle side-effects of setting an option.
@@ -4051,7 +4099,8 @@ static const char *did_set_option(OptIndex opt_idx, void *varp, Object old_value
// If option is hidden or if an error is detected, restore the previous value and don't do any
// further processing.
if (errmsg != NULL) {
set_option_varp(opt_idx, varp, old_value, true);
set_option_varp(opt_idx, varp, old_value);
optval_free(old_value);
// When resetting some values, need to act on it.
if (restore_chartab) {
buf_init_chartab(curbuf, true);
@@ -4061,7 +4110,7 @@ static const char *did_set_option(OptIndex opt_idx, void *varp, Object old_value
}
// Re-assign the new value as its value may get freed or modified by the option callback.
new_value = opt_from_varp(opt_idx, varp);
new_value = optval_own(opt->flags, opt_from_varp(opt_idx, varp));
if (set_sid != SID_NONE) {
sctx_T script_ctx = set_sid == 0 ? current_sctx : (sctx_T){ .sc_sid = set_sid };
@@ -4078,19 +4127,22 @@ static const char *did_set_option(OptIndex opt_idx, void *varp, Object old_value
// Global option with local value set to use global value.
// Free the local value and clear it.
void *varp_local = get_varp_scope(opt, OPT_LOCAL);
Object local_unset_value = get_option_unset_value(opt_idx);
set_option_varp(opt_idx, varp_local, copy_object(local_unset_value, NULL), true);
Object unset = get_option_unset_value(opt_idx);
set_option_varp(opt_idx, varp_local, unset);
optval_free_read(opt->flags, unset);
} else {
// May set global value for a buffer/window-local option. Skip when varp already is the
// global (a purely-global option): re-storing there is redundant, and for a callback option
// it would free the callback and then fail to re-parse an anonymous lambda's `<lambda>N`.
void *varp_global = get_varp_scope(opt, OPT_GLOBAL);
if (varp_global != varp) {
set_option_varp(opt_idx, varp_global, copy_object(new_value, NULL), true);
set_option_varp(opt_idx, varp_global, new_value);
}
}
}
optval_free(new_value); // Owned via optval_own() above.
// Don't do anything else if setting the option directly.
if (direct) {
return errmsg;
@@ -4170,9 +4222,13 @@ static const char *validate_option_value(const OptIndex opt_idx, Object *newval,
vimoption_T *opt = &options[opt_idx];
// Always allow unsetting local value of global-local option.
if (option_is_global_local(opt_idx) && (opt_flags & OPT_LOCAL)
&& option_equal(*newval, get_option_unset_value(opt_idx))) {
return NULL;
if (option_is_global_local(opt_idx) && (opt_flags & OPT_LOCAL)) {
Object unset = get_option_unset_value(opt_idx);
bool is_unset = option_equal(*newval, unset);
optval_free_read(opt->flags, unset);
if (is_unset) {
return NULL;
}
}
if (newval->type == kObjectTypeNil) {
@@ -4182,7 +4238,9 @@ static const char *validate_option_value(const OptIndex opt_idx, Object *newval,
if (opt_flags == OPT_GLOBAL) {
errmsg = _("Cannot unset global option value");
} else {
*newval = copy_object(get_option_unset_value(opt_idx), NULL);
Object unset = get_option_unset_value(opt_idx);
*newval = copy_object(unset, NULL);
optval_free_read(opt->flags, unset);
}
} else if (newval->type == kObjectTypeLuaRef) {
// A callback option accepts a funcref; scalar validation doesn't apply.
@@ -4290,26 +4348,29 @@ static const char *set_option(const OptIndex opt_idx, Object value, int opt_flag
void *varp_local = get_varp_scope(opt, OPT_LOCAL);
void *varp_global = get_varp_scope(opt, OPT_GLOBAL);
Object old_value = opt_from_varp(opt_idx, varp);
Object old_global_value = opt_from_varp(opt_idx, varp_global);
// If local value of global-local option is unset, use global value as local value.
Object old_local_value = is_opt_local_unset
? old_global_value
: opt_from_varp(opt_idx, varp_local);
// Value that's actually being used.
// For local scope of a global-local option, it's equal to the global value if the local value is
// unset. In every other case, it is the same as old_value.
// This value is used instead of old_value when triggering the OptionSet autocommand.
Object used_old_value = (scope_local && is_opt_local_unset)
? opt_from_varp(opt_idx, get_varp(opt))
: old_value;
// All values on the set path are owned.
Object old_value = optval_own(opt->flags, opt_from_varp(opt_idx, varp));
// Save the old values and the new value in case they get changed.
Object saved_used_value = copy_object(used_old_value, NULL);
Object saved_old_global_value = copy_object(old_global_value, NULL);
Object saved_old_local_value = copy_object(old_local_value, NULL);
// New value (and varp) may become invalid if the buffer is closed by autocommands.
Object saved_new_value = copy_object(value, NULL);
// Snapshots for OptionSet/UI reporting, taken before the store since it changes varp (and
// autocommands may even close the buffer). Skipped when not reporting (direct set).
Object saved_used_value = NIL;
Object saved_old_global_value = NIL;
Object saved_old_local_value = NIL;
Object saved_new_value = NIL;
if (!direct) {
saved_old_global_value = optval_snapshot_varp(opt_idx, varp_global);
// If local value of global-local option is unset, use global value as local value.
saved_old_local_value = is_opt_local_unset
? copy_object(saved_old_global_value, NULL)
: optval_snapshot_varp(opt_idx, varp_local);
// Value that's actually being used: for local scope of a global-local option, the global
// value if the local value is unset; in every other case, the same as old_value. Used instead
// of old_value when triggering the OptionSet autocommand.
saved_used_value = (scope_local && is_opt_local_unset)
? optval_snapshot_varp(opt_idx, get_varp(opt))
: optval_snapshot(old_value);
saved_new_value = optval_snapshot(value);
}
uint32_t *p = insecure_flag(curwin, opt_idx, opt_flags);
const int secure_saved = secure;
@@ -4321,8 +4382,8 @@ static const char *set_option(const OptIndex opt_idx, Object value, int opt_flag
secure = 1;
}
// Set option through its variable pointer.
set_option_varp(opt_idx, varp, value, false);
// Store the new value (copied) at varp.
set_option_varp(opt_idx, varp, value);
// Process any side effects.
errmsg = did_set_option(opt_idx, varp, old_value, value, opt_flags, set_sid, direct,
value_replaced, errbuf, errbuflen);
@@ -4345,6 +4406,9 @@ static const char *set_option(const OptIndex opt_idx, Object value, int opt_flag
optval_free(saved_old_global_value);
optval_free(saved_new_value);
// `old_value` is freed by did_set_option().
optval_free(value);
return errmsg;
}
@@ -4442,7 +4506,10 @@ const char *set_option_value(const OptIndex opt_idx, const Object value, int opt
static inline const char *unset_option_local_value(const OptIndex opt_idx)
{
assert(option_is_global_local(opt_idx));
return set_option_value(opt_idx, get_option_unset_value(opt_idx), OPT_LOCAL);
Object unset = get_option_unset_value(opt_idx);
const char *errmsg = set_option_value(opt_idx, unset, OPT_LOCAL);
optval_free_read(options[opt_idx].flags, unset);
return errmsg;
}
/// Set the value of an option. Supports TTY options, unlike set_option_value().
@@ -4758,7 +4825,9 @@ static int optval_default(OptIndex opt_idx, void *varp)
Object current_val = opt_from_varp(opt_idx, varp);
Object default_val = opt->def_val;
return option_equal(current_val, default_val);
bool is_default = option_equal(current_val, default_val);
optval_free_read(opt->flags, current_val);
return is_default;
}
/// Send update to UIs with values of UI relevant options
@@ -4958,17 +5027,20 @@ int makefoldset(FILE *fd)
/// @param varp Pointer to option variable.
///
/// @return FAIL on error, OK otherwise.
static int put_set(FILE *fd, char *cmd, OptIndex opt_idx, void *varp)
static int put_set_value(FILE *fd, char *cmd, OptIndex opt_idx, void *varp, Object value)
{
Object value = opt_from_varp(opt_idx, varp);
vimoption_T *opt = &options[opt_idx];
char *name = opt->fullname;
uint64_t flags = opt->flags;
if (option_is_global_local(opt_idx) && varp != opt->var
&& option_equal(value, get_option_unset_value(opt_idx))) {
// Processing unset local value of global-local option. Do nothing.
return OK;
if (option_is_global_local(opt_idx) && varp != opt->var) {
Object unset = get_option_unset_value(opt_idx);
bool is_unset = option_equal(value, unset);
optval_free_read(opt->flags, unset);
if (is_unset) {
// Processing unset local value of global-local option. Do nothing.
return OK;
}
}
switch (value.type) {
@@ -5067,6 +5139,17 @@ static int put_set(FILE *fd, char *cmd, OptIndex opt_idx, void *varp)
return OK;
}
/// Print the ":set" command for one option to a file, then release the read value (owned for a
/// callback option; see optval_free_read()).
static int put_set(FILE *fd, char *cmd, OptIndex opt_idx, void *varp)
{
Object value = opt_from_varp(opt_idx, varp);
// Skip func-option set to a Lua function (omit from :mksession/etc.).
int ret = value.type == kObjectTypeLuaRef ? OK : put_set_value(fd, cmd, opt_idx, varp, value);
optval_free_read(options[opt_idx].flags, value);
return ret;
}
void *get_varp_scope_from(vimoption_T *p, int opt_flags, buf_T *buf, win_T *win)
{
OptIndex opt_idx = get_opt_idx(p);
@@ -7057,8 +7140,11 @@ dict_T *get_winbuf_options(const int bufopt)
void *varp = get_varp(opt);
if (varp != NULL) {
typval_T opt_tv = opt_to_tv(opt_from_varp(opt_idx, varp), true);
Object ov = opt_from_varp(opt_idx, varp);
typval_T opt_tv = opt_to_tv(ov, true);
tv_dict_add_tv(d, opt->fullname, strlen(opt->fullname), &opt_tv);
tv_clear(&opt_tv);
optval_free_read(opt->flags, ov);
}
}
}

View File

@@ -7741,7 +7741,7 @@ local options = {
localoptions options and mappings local to a window or buffer (not
global values for local options)
options all options and mappings (also global values for local
options)
options), except Lua functions |option-value-function|.
skiprtp exclude 'runtimepath' and 'packpath' from the options
resize size of the Vim window: 'lines' and 'columns'
sesdir the directory in which the session file is located
@@ -10419,7 +10419,8 @@ local options = {
folds manually created folds, opened/closed folds and local
fold options
options options and mappings local to a window or buffer (not
global values for local options)
global values for local options), except Lua functions
|option-value-function|.
localoptions same as "options"
slash |deprecated| Always enabled. Uses "/" in filenames.
unix |deprecated| Always enabled. Uses "\n" line endings.

View File

@@ -1843,7 +1843,9 @@ static const char *did_set_statustabline_rulerformat(optset_T *args, bool rulerf
&& ((args->os_flags & OPT_GLOBAL) || !(args->os_flags & OPT_LOCAL))
&& s[0] == NUL) {
xfree(*varp);
*varp = xstrdup(get_option_default(args->os_idx, args->os_flags).data.string.data);
Object def = get_option_default(args->os_idx, args->os_flags);
*varp = xstrdup(def.data.string.data);
optval_free_read(get_option_flags(args->os_idx), def);
s = *varp;
}

View File

@@ -2172,7 +2172,9 @@ stcsign:
// matter?
// if (called_emsg > called_emsg_before)
if (opt_idx != kOptInvalid && did_emsg > did_emsg_before) {
set_option_direct(opt_idx, get_option_default(opt_idx, opt_scope), opt_scope, SID_ERROR);
Object def = get_option_default(opt_idx, opt_scope);
set_option_direct(opt_idx, def, opt_scope, SID_ERROR);
optval_free_read(get_option_flags(opt_idx), def);
}
// A user function may reset KeyTyped, restore it.

View File

@@ -129,7 +129,7 @@ describe('callback (func/expr) options', function()
it('accept a Lua function, preserving identity and captured upvalues', function()
-- 'operatorfunc' is global; 'tagfunc' is buffer-local: exercise both storage paths.
eq(
{ identity = true, captured = 'CAP', kind = 'function' },
{ identity = true, captured = 'CAP', kind = 'function', optset_old = '', optset_new = true },
exec_lua(function()
vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'abcdef' })
local captured = 'CAP'
@@ -137,12 +137,22 @@ describe('callback (func/expr) options', function()
local fn = function(_)
_G.seen = captured
end
-- OptionSet emits with Lua function as "<Lua …>".
local optset
vim.api.nvim_create_autocmd('OptionSet', {
pattern = 'operatorfunc',
callback = function()
optset = { old = vim.v.option_old, new = vim.v.option_new }
end,
})
vim.o.operatorfunc = fn
vim.cmd('normal! g@l')
return {
identity = vim.o.operatorfunc == fn,
captured = _G.seen,
kind = type(vim.o.operatorfunc),
optset_old = optset.old,
optset_new = tostring(optset.new):match('^<Lua ') ~= nil,
}
end)
)