`decl` with value vs `set`; `update-option`

It seems that using decl int int_opt VALUE can be used anywhere in code, at any point during runtime (not just at load-time); it seems to be the same as set global, without needing to pre-declare the option. Are there any differences?

Also, WTH does update-option do? The docstring (as opposed to the actual docs) is the least vague reference I could find (without actually being clear); it says “some option types, such as line-specs or range-specs can be updated to latest buffer timestamp”. Do options have hidden timestamp meta-data? I see update-option being used on user-defined options (e.g. clang_errors), so how can kak know what semantics that option has?

It’s possible to use declare-option multiple times without any kind of explicit -override such as define-function or provide-module require. This is generally used when modules should have common configuration but they don’t want to depend on one another - for example, make.kak, lint.kak and grep.kak all declare the toolsclient option, but none of them is the “real” owner. It’s a bit less important now that Kakoune has provide-module, but adding a new module just to contain the toolsclient option seems a bit of overkill.

Do options have hidden timestamp meta-data?

Options in general don’t, but some specific option types do. Looking at the line-specs and range-specs option types in :doc options:

  • line-specs
    • a list of a line number and a corresponding flag […], except for the first element which is just the timestamp of the buffer.
  • range-specs
    • a timestamp […] followed by a list of range descriptors.

Let’s say a buffer has 10 lines, and a plugin sets a line-specs option to display a flag on line 9. Later, the user deletes the first three lines of the buffer, so it only has 7 lines… but the line-specs option doesn’t magically change its value, it still says line 9.

Kakoune’s solution is for options that describe parts of the buffer to include a timestamp, so that the coordinates are interpreted as of that timestamp rather than assuming the describe the current state of the buffer. If the timestamp in the option is not the current timestamp of the buffer, Kakoune walks back through the buffer’s undo history to figure out what deltas it needs to apply to the option values to translate them to the current buffer state. update-option does that transformation once and updates the option, so that Kakoune doesn’t have to repeatedly do that same transformation every time it repaints the screen. It’s also useful when a plugin wants to learn about changes that have occurred in a buffer - it can set a line-specs or range-specs option to describe the buffer parts it’s interested in, then later call update-option and read the value back to see how they’ve changed.

If you try to update-option an option whose type doesn’t include a timestamp (like an int or string option) you’ll get an error.

1 Like

I kinda guessed the rationale behind decl's current behavior. But more interesting is that you can call it from a def. At runtime (e.g. in response to a user command, or from a hook). Without any declaration during source. In this case, is it equivalent to set global? set current? set buffer?

Kakoune’s solution is for options that describe parts of the buffer to include a timestamp

Ah yes, so not implicit, but explicit metadata. I’ve had to deal with that in sel-editor, which basically zip()s the contents of reg{.} and %val{selections}, encapsulates the text in ||, and displays it in a separate buffer. Then when the user modifies either the ranges or the contents of the selections, it tries to update the original buffer, which sometimes fails. Maybe I need to use update-selections, and probably specially-typed options.

Anyway, thanks; perhaps your explanation (as often seems the case) should make it into the manual :slight_smile:

[for some reason the quote-reply disappeared]

The “global” scope is special for a few reasons:

  • you can’t unset-option at global scope
  • you can’t set-option at global scope unless that option was already set in that scope
  • you can only declare-option at global scope
  • every option has a value at global scope, even if you didn’t provide a default (integers default to 0, strings default to empty, etc.)

In a language like C, “variable declarations” are very distinct from “variable values”. Declarations only exist in the source code; values only exist in the running program. Meanwhile in Kakoune, the command for declaring an option (declare-option) is separate from the commands for modifying values (set-option and unset-option) so you might expect that the same wide separation exists there too.

I haven’t actually checked the code myself, but I strongly suspect that separation is not present. Actually, looking at the quirks of global scope listed above, I suspect that it is used as the authoritative list of both variable declarations and values, and the limitations of declare-option, set-option and unset-option are only there to present the illusion that declarations are separate from values. So yeah, the reason declare-option seems like it overlaps with set-option is that it does, it’s just that the two commands enforce different pre-conditions.

you can’t set-option at global scope unless that option was already set in that scope

I’m not sure what this means. Once you declare an opt with decl (with or without an initial value), you can also set global right away. What would be an example causing an error?

  • you can only declare-option at global scope

decl simply has no scope argument, so what does this restriction mean…?

Let’s assume we have some C++ OptionVariant type that can store any kind of Kakoune option (int, bool, str, coord, range-specs, etc.) and can report which variant it is currently storing. Then, I imagine each scope is a Hashmap<String, OptionVariant>, mapping an option name to a variant. Such a scope would have the usual operations:

  • scope.get(name) to retrieve the current variant associated with the named option
  • scope.set(name, variant) to add a new option, or replace the previous value
  • scope.remove(name) to remove an option from the scope

But Kakoune’s option-manipulation commands don’t map cleanly to these basic operations. Instead, the commands wrap those operations with different checks:

def declare_option(name, opt_type, opt_value):
    GLOBAL_SCOPE.set(name, OptionVariant(opt_type, opt_value))

def set_option(scope, name, opt_value):
    if name not in GLOBAL_SCOPE:
        raise "no such option"
    opt_type = GLOBAL_SCOPE.get(name).opt_type
    scope.set(name, OptionVariant(opt_type, opt_value))

def unset_option(scope, name):
    if scope == GLOBAL_SCOPE:
        raise "can't unset from global scope"
    if name not in scope:
        return # already done!
    scope.remove(name)

I was trying to hint at the checks I’ve written out explicitly in the pseudo-code above. declare-option sets an option unconditionally, but can only modify global scope. set-option can modify any scope, but requires the option be already set at global scope (by declare-option).

A good question might be why Kakoune provides option-manipulation commands that so awkwardly map to the underlying data-structure operations. I suspect it’s because although the commands can interact oddly with each other, plugins built on these commands are far less likely to break each other at runtime.

1 Like

Ah no, there is a catch.

decl str-list slist a b
echo -debug %opt{slist}  # a b
decl str-list slist a b c
echo -debug %opt{slist}  # a b c
decl str-list slist
echo -debug %opt{slist}  # still a b c
# %arg{@} from command prompt is an empty list
decl str-list slist %arg{@}  # still a b c

So you can’t reset a list to empty using decl. And if you try to assign to it from another source, which sometimes turns out empty, you have a nice new untrackable bug.

This is similar to the reg t %opt{maybe_empty} inconsistency: it assigns a list, but if the list is empty, it assigns the empty string.