Minga.Keymap.Bindings (Minga v0.1.0)

Copy Markdown View Source

Prefix tree (trie) for key sequence → command bindings.

Each node in the trie can represent either an intermediate step in a multi-key sequence (a prefix node) or a terminal binding (a command node). Nodes may simultaneously be a prefix and a command (e.g. g could be a command and also prefix for gg).

Key representation

A key is a {codepoint, modifiers} tuple where codepoint is the Unicode codepoint of the key and modifiers is a bitmask of modifier keys:

  • 0x01 — Shift
  • 0x02 — Ctrl
  • 0x04 — Alt
  • 0x08 — Super

Usage

trie = Minga.Keymap.Bindings.new()
trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :file_start, "Go to first line")

{:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
{:prefix, node}        = Minga.Keymap.Bindings.lookup(trie, {?g, 0})

Summary

Types

A single key event: {codepoint, modifiers}.

A trie node.

Functions

Binds a key sequence to a command in the trie.

Sets a human-readable description on an intermediate (prefix) node without binding a command. Useful for labelling leader-key groups like f → "+file".

Returns the direct children of a trie node for which-key display.

Formats a single key/0 tuple into a human-readable string.

Looks up a single key in the trie.

Looks up a full key sequence in the trie, walking node by node.

Merges a list of binding tuples into a trie.

Merges a list of binding tuples into a trie, excluding specific commands.

Merges a named shared group into a trie.

Merges a named shared group into a trie with exclusions.

Creates a new, empty trie root node.

Removes a key sequence binding from the trie.

Types

key()

@type key() :: {codepoint :: non_neg_integer(), modifiers :: non_neg_integer()}

A single key event: {codepoint, modifiers}.

codepoint is the Unicode codepoint (e.g. ?j = 106). modifiers is a bitmask: Shift=0x01, Ctrl=0x02, Alt=0x04, Super=0x08.

node_t()

@type node_t() :: Minga.Keymap.Bindings.Node.t()

A trie node.

Functions

bind(root, list, command, description)

@spec bind(node_t(), [key()], atom() | tuple(), String.t()) :: node_t()

Binds a key sequence to a command in the trie.

Returns an updated trie root. Intermediate nodes are created as needed. Rebinding an existing sequence overwrites the previous binding.

Parameters

  • root — the trie root node
  • keys — non-empty list of key/0 values representing the sequence
  • command — atom name of the command to bind
  • description — human-readable description for which-key display

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
{:command, :move_down}
iex> Minga.Keymap.Bindings.lookup(trie, {?k, 0})
:not_found

bind_prefix(root, list, description)

@spec bind_prefix(node_t(), [key()], String.t()) :: node_t()

Sets a human-readable description on an intermediate (prefix) node without binding a command. Useful for labelling leader-key groups like f → "+file".

Creates intermediate nodes as needed.

children(node)

@spec children(node_t()) :: [{key(), String.t() | atom()}]

Returns the direct children of a trie node for which-key display.

Each entry is a {key, label} tuple where label is either the description string (for a terminal binding) or the command atom (for a prefix or unnamed node).

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
iex> Minga.Keymap.Bindings.children(trie)
[{{106, 0}, "Move cursor down"}]

format_key(arg)

@spec format_key(key()) :: String.t()

Formats a single key/0 tuple into a human-readable string.

Examples

iex> Minga.Keymap.Bindings.format_key({32, 0})
"SPC"

iex> Minga.Keymap.Bindings.format_key({?s, 0x02})
"C-s"

iex> Minga.Keymap.Bindings.format_key({?j, 0x00})
"j"

lookup(node, key)

@spec lookup(node_t(), key()) ::
  {:command, atom() | tuple()} | {:prefix, node_t()} | :not_found

Looks up a single key in the trie.

Returns one of:

  • {:command, atom()} — the key sequence is complete and maps to a command
  • {:prefix, node_t()} — the key is a valid prefix; the returned node can be used as the new root for the next key
  • :not_found — the key does not exist in this trie node

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :document_start, "Go to first line")
iex> match?({:prefix, _}, Minga.Keymap.Bindings.lookup(trie, {?g, 0}))
true
iex> Minga.Keymap.Bindings.lookup(trie, {?z, 0})
:not_found

lookup_sequence(node, list)

@spec lookup_sequence(node_t(), [key()]) ::
  {:command, atom(), String.t()} | {:prefix, node_t()} | :not_found

Looks up a full key sequence in the trie, walking node by node.

Returns one of:

  • {:command, atom(), String.t()} — the sequence maps to a command with its description
  • {:prefix, node_t()} — the sequence is a valid prefix (more keys needed)
  • :not_found — no match at some point in the sequence

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :document_start, "Go to first line")
iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?g, 0}, {?g, 0}])
{:command, :document_start, "Go to first line"}
iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?g, 0}])
{:prefix, %Minga.Keymap.Bindings.Node{children: %{{103, 0} => %Minga.Keymap.Bindings.Node{children: %{}, command: :document_start, description: "Go to first line"}}, command: nil, description: nil}}
iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?z, 0}])
:not_found

merge_bindings(trie, bindings)

@spec merge_bindings(node_t(), [{[key()], atom() | tuple(), String.t()}]) :: node_t()

Merges a list of binding tuples into a trie.

Each binding is a {key_sequence, command, description} tuple. Bindings are applied in order, so later entries override earlier ones on conflict.

This is the bulk registration helper for shared binding groups. Scope modules call this to include a group's bindings, then apply scope-specific bindings on top (which override group bindings on conflict).

Examples

iex> bindings = [
...>   {[{?j, 0}], :move_down, "Move down"},
...>   {[{?k, 0}], :move_up, "Move up"}
...> ]
iex> trie = Minga.Keymap.Bindings.merge_bindings(Minga.Keymap.Bindings.new(), bindings)
iex> {:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
iex> {:command, :move_up} = Minga.Keymap.Bindings.lookup(trie, {?k, 0})

merge_bindings(trie, bindings, opts)

@spec merge_bindings(node_t(), [{[key()], atom() | tuple(), String.t()}], keyword()) ::
  node_t()

Merges a list of binding tuples into a trie, excluding specific commands.

Same as merge_bindings/2 but skips any binding whose command atom appears in the exclude list. Use this when a scope includes a shared group but needs to override specific commands with different semantics.

Examples

iex> bindings = [
...>   {[{?j, 0}], :move_down, "Move down"},
...>   {[{?q, 0}], :quit_editor, "Quit"}
...> ]
iex> trie = Minga.Keymap.Bindings.merge_bindings(Minga.Keymap.Bindings.new(), bindings, exclude: [:quit_editor])
iex> {:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
iex> :not_found = Minga.Keymap.Bindings.lookup(trie, {?q, 0})

merge_group(trie, group_name)

@spec merge_group(node_t(), atom()) :: node_t()

Merges a named shared group into a trie.

Convenience wrapper that calls SharedGroups.get/1 and merge_bindings/2.

Examples

trie = Bindings.new()
|> Bindings.merge_group(:cua_navigation)
|> Bindings.bind([{?q, 0}], :quit, "Quit")

merge_group(trie, group_name, opts)

@spec merge_group(node_t(), atom(), keyword()) :: node_t()

Merges a named shared group into a trie with exclusions.

Examples

trie = Bindings.new()
|> Bindings.merge_group(:cua_navigation, exclude: [:move_up])

new()

@spec new() :: node_t()

Creates a new, empty trie root node.

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
:not_found

unbind(root, list)

@spec unbind(node_t(), [key()]) :: node_t()

Removes a key sequence binding from the trie.

Clears the command and description on the terminal node. Prunes empty intermediate nodes (nodes with no command and no children) on the way back up so the trie doesn't accumulate dead branches.

Returns the updated trie. No-op if the sequence doesn't exist.

Examples

iex> trie = Minga.Keymap.Bindings.new()
iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
iex> trie = Minga.Keymap.Bindings.unbind(trie, [{?j, 0}])
iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
:not_found