Selection Sets, do mathematical set operations on selections

Like many people over the years, I’ve been confused by Kakoune’s combine-selection operations (available via <a-z> and <a-Z>). Although they’re named things like “union” and “intersection”, they don’t behave in the way I’d expect those operations to act: they work on paired selection-spans, rather than treating all the selected text as a single set.

And so, I decided to make a plugin that implements “mathematical” set operations:

(this plugin was developed with Python 3.9, but it should work on Python 3.7 or above)

With the user mode mappings mentioned in the README, you can use ,z and ,Z instead of <a-z> and <a-Z> to do mathematical-set union and intersection of the current selection with a stored selection. There’s also a :selection-sets-apply command you can use to apply these operations from a script, since execute-keys doesn’t work so well with plugins.

To illustrate these operations, I’ll quote from :doc selection-sets:

Let’s say we have a document like this:

    ||||||  -------
  ||||||||++---------
||||||||++++++---------
||||||||++++++---------
||||||||++++++---------
  ||||||||++---------
    ||||||  -------

…​where the | and + are currently selected:

    XXXXXX  -------
  XXXXXXXXXX---------
XXXXXXXXXXXXXX---------
XXXXXXXXXXXXXX---------
XXXXXXXXXXXXXX---------
  XXXXXXXXXX---------
    XXXXXX  -------

…​and the + and - are in the saved selection:

    ||||||  XXXXXXX
  ||||||||XXXXXXXXXXX
||||||||XXXXXXXXXXXXXXX
||||||||XXXXXXXXXXXXXXX
||||||||XXXXXXXXXXXXXXX
  ||||||||XXXXXXXXXXX
    ||||||  XXXXXXX

Union

The result of applying the union operator is all the text in either the current or saved selections:

    XXXXXX  XXXXXXX
  XXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXX
  XXXXXXXXXXXXXXXXXXX
    XXXXXX  XXXXXXX

Intersection

The result of applying the intersection operator is all the text in both the current and saved selections:

    ||||||  -------
  ||||||||XX---------
||||||||XXXXXX---------
||||||||XXXXXX---------
||||||||XXXXXX---------
  ||||||||XX---------
    ||||||  -------

Implementation notes

Unfortunately, unlike the goto (g), view (v), or object (<a-i>, <a-a>) sub-modes, it’s not (yet) possible to add custom mappings to the combine-selections (<a-z> or <a-Z>) sub-mode. But we can create our own operation menu as a custom user mode, right?

Unfortunately, unlike normal mode and user mode, the %val{register} and %val{count} expansions aren’t available to mappings in custom user modes. Since it’s important to support saving the selection to different registers, I had to fake up my own menu with :info and :on-key.

While it’s good that a mapping can use %val{register} to read the pending custom register, actually reading the register is a bit of a pain. I would up having to use the double-eval trick in a couple of places, and to avoid a mess of quoting I wound up sticking the “inner” code into a helper function, like this:

def process-register -params 1 %{
    eval "process-register-inner %%reg{%arg{1}}"
}

def process-register-inner -hidden -params .. %{
    eval %sh{
        # OK, the register contents are in "$@", now we can process them
    }
}

This works quite nicely (especially since there’s no quoting hazard in any of the register names I care about), but it does break up the implementation into more, smaller functions than feels comfortable.

Although I generally try to keep my Kakoune plugins aggressively simple, I felt I needed Python’s iterators for this one… and since I was using Python anyway, I fell down the rabbit hole of unit-testing and the new (Python 3.5+) optional static typing hints, winding up trying to write Python like it was Haskell, or at least Rust. As a result, the Python helper script is nearly 700 lines long, of which only the first 269 are used at runtime, and only about 60 lines (less than 10% of the total!) are the actual algorithms. I bet there’s a smaller, faster, more portable way to do this.

The future

I’d love to support more set operations. I’ll probably get around to “left difference”, “right difference” and “symmetric difference” eventually, but I’d be happy to receive PRs for them, or for other operations people find helpful. As a reference, you can look at what it took to implement intersection.

12 Likes

This is brilliant! Nice work and nice idea, Screwtape.

I think I’ll find a way to have both selection combinators available—your way and the traditional way. It won’t be much work to add a toggle, I’m sure.

Although I generally try to keep my Kakoune plugins aggressively simple, I felt I needed Python’s iterators for this one… and since I was using Python anyway, I fell down the rabbit hole of unit-testing and the new (Python 3.5+) optional static typing hints, winding up trying to write Python like it was Haskell, or at least Rust. As a result, the Python helper script is nearly 700 lines long, of which only the first 269 are used at runtime, and only about 60 lines (less than 10% of the total!) are the actual algorithms. I bet there’s a smaller, faster, more portable way to do this.

Huh, so I’m not the only one! It’s hard choosing a language to write external scripts in, isn’t it?

I wrote an alternative implementation back when this was posted but only sent it on discord, so here goes: kakconf/invert.kak at main · danr/kakconf · GitHub

1 Like