Class: Rubino::UI::CompletionMenu
- Inherits:
-
Object
- Object
- Rubino::UI::CompletionMenu
- Defined in:
- lib/rubino/ui/completion_menu.rb
Overview
The BottomComposer‘s /command + @file completion menu: an inline navigable list rendered in the multi-row live region above the prompt. Candidates come from the shared CompletionSource. The menu auto-opens as you type a `/` or `@` token (Reline parity); Tab also opens/accepts, ↑/↓navigate, Enter accepts, ESC dismisses immediately (and STICKS for the token) leaving the typed buffer untouched.
Pure state machine + row formatting: it reads the buffer/cursor the composer passes in and never prints or takes the render mutex — opening, navigation and the accept SPLICE are decided here, but the composer applies the splice to its buffer and owns every redraw.
Constant Summary collapse
- MAX_ROWS =
Most candidate rows shown at once (the list scrolls within this window for longer candidate sets so the prompt is never pushed off-screen).
8
Instance Method Summary collapse
-
#accept_splice ⇒ Object
Accept the highlighted candidate: returns the splice the composer applies — [start, token_len, replacement] where the replacement carries a trailing space (so the next token starts clean, like Reline’s append char) — and closes the menu (clearing the sticky dismiss: accepting ends this token; a new one can auto-open).
-
#auto_update(buffer, cursor) ⇒ Object
Open / update / close the menu on every edit and cursor move, matching the old Reline autocompletion: typing a leading ‘/` or `@` token AUTO-opens the dropdown (no Tab needed), refining as the token grows and closing when it no longer completes.
-
#close! ⇒ Object
Close the menu and clear the sticky ESC-dismiss flag (submit / accept): the next token starts fresh and is free to auto-open again.
-
#dismiss! ⇒ Object
Lone-ESC dismiss: close AND STICK for the current token so it doesn’t pop back on the next keystroke.
- #down ⇒ Object
-
#exact_command?(buffer) ⇒ Boolean
True when the buffer is ALREADY an exact, complete command, so Enter should SUBMIT it rather than accept-and-space (D5/#147).
-
#hide! ⇒ Object
Teardown hide (composer stop/suspend): close the rows without touching the sticky dismiss, so a resume mid-token behaves exactly as before.
-
#initialize(completion_source) ⇒ CompletionMenu
constructor
A new instance of CompletionMenu.
-
#items ⇒ Object
The open menu’s candidate items (test/inspection helper), nil when closed.
-
#open(buffer, cursor) ⇒ Object
Explicit open (Tab): always clears a sticky ESC-dismiss first — a deliberate Tab reopens a dismissed menu — then opens for the completion context under the cursor, if any.
- #open? ⇒ Boolean
-
#rows(cols) ⇒ Object
The rendered menu rows (the slice in view, the selected one marked with a cyan ❯ and inverse highlight), or [] when no menu is open.
-
#up ⇒ Object
↑/↓ within the open menu (routed from the composer’s history keys).
Constructor Details
#initialize(completion_source) ⇒ CompletionMenu
Returns a new instance of CompletionMenu.
26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/rubino/ui/completion_menu.rb', line 26 def initialize(completion_source) @completion = completion_source # Open state: nil when closed, else a Hash with the candidate :items, # the :selected index, the :top of the visible window, the :token span # being completed (so accept can splice the replacement at the cursor) # and the :navigated accept-intent flag. @state = nil # Sticky ESC-dismiss: once the user presses ESC on an open menu, keep # it closed for the CURRENT token instead of re-opening on the next # keystroke. Cleared when the token is cleared / on submit / on accept / # on an explicit Tab, so a fresh token (or a deliberate Tab) reopens. @suppressed = false end |
Instance Method Details
#accept_splice ⇒ Object
Accept the highlighted candidate: returns the splice the composer applies — [start, token_len, replacement] where the replacement carries a trailing space (so the next token starts clean, like Reline’s append char) — and closes the menu (clearing the sticky dismiss: accepting ends this token; a new one can auto-open).
112 113 114 115 116 117 |
# File 'lib/rubino/ui/completion_menu.rb', line 112 def accept_splice choice = @state[:items][@state[:selected]].to_s splice = [@state[:start], @state[:token_len], "#{choice} "] close! splice end |
#auto_update(buffer, cursor) ⇒ Object
Open / update / close the menu on every edit and cursor move, matching the old Reline autocompletion: typing a leading ‘/` or `@` token AUTO-opens the dropdown (no Tab needed), refining as the token grows and closing when it no longer completes. Called from every buffer-edit and cursor-move path so the list always tracks the token under the cursor.
* no token under the cursor → close the menu AND clear the sticky
ESC-dismiss flag (a fresh token may auto-open again);
* token present but ESC-dismissed for it → stay closed;
* token with candidates → OPEN a new menu, or UPDATE an open one
(preserving the clamped selection); no candidates → close.
The selected index is preserved (clamped) across an update so refining the token doesn’t jump the highlight back to the top mid-navigation.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/rubino/ui/completion_menu.rb', line 77 def auto_update(buffer, cursor) ctx = completion_context(buffer, cursor) if ctx.nil? @state = nil @suppressed = false # token cleared: a fresh token can auto-open return end return if @suppressed # ESC stuck this token/argument closed items, start, len = ctx sel = (@state ? @state[:selected] : 0).clamp(0, items.size - 1) @state = { items: items, selected: sel, top: window_top(sel, items.size), start: start, token_len: len, navigated: @state ? @state[:navigated] : false } end |
#close! ⇒ Object
Close the menu and clear the sticky ESC-dismiss flag (submit / accept): the next token starts fresh and is free to auto-open again.
147 148 149 150 |
# File 'lib/rubino/ui/completion_menu.rb', line 147 def close! @state = nil @suppressed = false end |
#dismiss! ⇒ Object
Lone-ESC dismiss: close AND STICK for the current token so it doesn’t pop back on the next keystroke. Cleared when the token changes to nil, on submit/accept, or on an explicit Tab (see #auto_update / #close!).
155 156 157 158 |
# File 'lib/rubino/ui/completion_menu.rb', line 155 def dismiss! @state = nil @suppressed = true end |
#down ⇒ Object
102 103 104 105 |
# File 'lib/rubino/ui/completion_menu.rb', line 102 def down @state[:selected] = [@state[:selected] + 1, @state[:items].size - 1].min navigated_to_selection end |
#exact_command?(buffer) ⇒ Boolean
True when the buffer is ALREADY an exact, complete command, so Enter should SUBMIT it rather than accept-and-space (D5/#147). Compares the TOKEN the menu would splice (not the whole buffer, which never matches a bare argument candidate — that’s what swallowed Enter on a fully typed ‘/agents sa_xxx`): submit when the typed token equals a candidate exactly AND that match is the menu’s current selection (or the only candidate) — so a partial/ambiguous token (e.g. “/re” with /reasoning + /reset) still accepts the highlight on Enter as before. An EMPTY argument token (‘/agents sa_xxx ` with the verb dropdown open) also submits — the buffer is already a complete command and accepting would splice a verb the user never typed — UNLESS the user explicitly arrow-navigated onto a candidate, which is an accept intent. Tab-accept is untouched.
132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/rubino/ui/completion_menu.rb', line 132 def exact_command?(buffer) return false unless @state typed = Array(buffer.chars[@state[:start], @state[:token_len]]).join return !@state[:navigated] if typed.empty? items = @state[:items] return false unless items.include?(typed) selected = items[@state[:selected]].to_s items.size == 1 || selected == typed end |
#hide! ⇒ Object
Teardown hide (composer stop/suspend): close the rows without touching the sticky dismiss, so a resume mid-token behaves exactly as before.
162 163 164 |
# File 'lib/rubino/ui/completion_menu.rb', line 162 def hide! @state = nil end |
#items ⇒ Object
The open menu’s candidate items (test/inspection helper), nil when closed.
45 46 47 |
# File 'lib/rubino/ui/completion_menu.rb', line 45 def items @state && @state[:items] end |
#open(buffer, cursor) ⇒ Object
Explicit open (Tab): always clears a sticky ESC-dismiss first — a deliberate Tab reopens a dismissed menu — then opens for the completion context under the cursor, if any. Returns the opened state (truthy; the composer redraws on it), or nil when nothing completes here.
53 54 55 56 57 58 59 60 61 |
# File 'lib/rubino/ui/completion_menu.rb', line 53 def open(buffer, cursor) @suppressed = false ctx = completion_context(buffer, cursor) return unless ctx items, start, len = ctx @state = { items: items, selected: 0, top: 0, start: start, token_len: len, navigated: false } end |
#open? ⇒ Boolean
40 41 42 |
# File 'lib/rubino/ui/completion_menu.rb', line 40 def open? !@state.nil? end |
#rows(cols) ⇒ Object
The rendered menu rows (the slice in view, the selected one marked with a cyan ❯ and inverse highlight), or [] when no menu is open. House grammar: a dim aside bar leads each row. Candidates with a registered description (BuiltIns/custom command one-liners, the /agents subcommand hints) show it dim in an aligned column next to the name (#39).
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/rubino/ui/completion_menu.rb', line 171 def rows(cols) return [] unless @state items = @state[:items] top = @state[:top] sel = @state[:selected] slice = items[top, MAX_ROWS] || [] pad = slice.map { |item| LiveRegion.display_width(item.to_s) }.max.to_i rows = slice.each_with_index.map do |item, i| candidate_row(item, pad, cols, selected: top + i == sel) end rows << pastel.dim("┄ #{sel + 1}/#{items.size} ┄") if items.size > MAX_ROWS rows end |
#up ⇒ Object
↑/↓ within the open menu (routed from the composer’s history keys). Arrowing marks the menu as NAVIGATED — an explicit accept intent, so Enter on an empty argument token accepts the highlight instead of submitting the buffer (see #exact_command?).
97 98 99 100 |
# File 'lib/rubino/ui/completion_menu.rb', line 97 def up @state[:selected] = [@state[:selected] - 1, 0].max navigated_to_selection end |