Module: Irb::Autosuggestions::LineEditorPatch
- Defined in:
- lib/irb/autosuggestions/line_editor_patch.rb,
sig/lib/irb/autosuggestions/line_editor_patch.rbs
Overview
Patches Reline::LineEditor to display fish-like autosuggestions from history. rubocop:disable Metrics/ModuleLength, SortedMethodsByCall/Waterfall
Constant Summary collapse
- GRAY =
"\e[90m"- DIM =
"\e[2m"- RESET_COLOR =
"\e[39;49m"- RESET =
"\e[0m"- FG_COLORS =
((30..37).to_a + (90..97).to_a + [38, 39]).freeze
- CONFIG_KEY =
:USE_AUTOSUGGESTIONS- ENV_KEY =
'IRB_AUTOSUGGESTIONS'- CONFIG_NAV_KEY =
:USE_PREFIX_HISTORY_NAVIGATION- ENV_NAV_KEY =
'IRB_PREFIX_HISTORY_NAVIGATION'- ACCEPT_KEYS_CONFIG =
:AUTOSUGGESTION_ACCEPT_KEYS- DEFAULT_ACCEPT_KEYS =
%i[ed_next_char ed_end_of_line tab].freeze
- GHOST_COLOR_CONFIG =
:AUTOSUGGESTION_GHOST_COLOR- GHOST_STYLE_CONFIG =
:AUTOSUGGESTION_GHOST_STYLE- DEFAULT_GHOST_COLOR =
"\e[90m"- FG_MAP =
{ black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, default: 39, bright_black: 90, bright_red: 91, bright_green: 92, bright_yellow: 93, bright_blue: 94, bright_magenta: 95, bright_cyan: 96, bright_white: 97 }.freeze
- ATTR_MAP =
{ bold: 1, dim: 2, italic: 3, underline: 4, blink: 5, reverse: 7 }.freeze
Instance Method Summary collapse
-
#accept_key?(key) ⇒ Boolean
Checks if a key event matches any configured accept key.
-
#accept_matching_suggestion? ⇒ Boolean
Accepts the current suggestion when it extends the buffer.
-
#accept_suggestion(suggestion) ⇒ void
Replaces the entire buffer with the accepted suggestion and triggers a rerender.
-
#colorize_ghost_lines(ghost, suggestion, ghost_byte_start = nil) ⇒ Array<String>
Colorizes the full suggestion and extracts the ghost portion.
-
#dim_line(line) ⇒ String
Prepends each ANSI foreground color code with
2;(dim) and strips non-color attributes (bold, underline, reverse…). -
#enabled? ⇒ Boolean
Checks whether autosuggestions are enabled via IRB.conf or env var.
-
#extract_ansi_colored_suffix(colored_text, visible_byte_offset) ⇒ String
Extracts the suffix of an ANSI-colored string starting at a given visible byte offset, preserving all ANSI codes.
-
#find_next_match(buffer, from_pointer) ⇒ Integer?
Finds the index of the next (newer) history entry starting with
buffer. -
#find_prev_match(buffer, from_pointer) ⇒ Integer?
Finds the index of the previous (older) history entry starting with
buffer. -
#find_suggestion(buffer) ⇒ String?
Finds the most recent history entry that starts with the given buffer.
-
#ghost_color ⇒ String
Returns the effective ANSI color code for ghost text.
-
#ghost_display_lines(ghost, suggestion, ghost_byte_start = nil) ⇒ Array<String>
Returns ghost lines ready for terminal output (with ANSI codes).
-
#input_key(key) ⇒ Object
Intercepts key input to accept autosuggestions on configured keys and clears the prefix navigation anchor on non-history keys.
-
#key_match?(key, config_symbol) ⇒ Boolean
Checks if a key matches a given config symbol.
-
#navigation_enabled? ⇒ Boolean
Whether prefix-filtered history navigation is enabled.
- #render ⇒ Object
-
#render_ghost(ghost, suggestion = nil, ghost_byte_start = nil) ⇒ void
Writes the ghost text (inline + extra lines) to terminal output.
-
#render_ghost_suggestion ⇒ void
Renders ghost text for the current buffer, if a suggestion exists.
-
#rerender ⇒ Object
Injects ghost text into terminal output after Reline finishes rendering.
-
#resolve_ghost_style(hash) ⇒ String
Converts a style hash like
{ fg: :bright_black, italic: true }to an ANSI escape sequence. -
#use_colorize? ⇒ Boolean
Checks whether syntax coloring is available and enabled.
-
#write_extra_ghost_lines(lines) ⇒ void
Writes extra ghost lines below the current buffer line with prompt-width alignment.
Instance Method Details
#accept_key?(key) ⇒ Boolean
Checks if a key event matches any configured accept key. Defaults to right arrow (:ed_next_char). Tab is matched specially — method_symbol == :ed_insert + char == "\t".
248 249 250 251 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 248 def accept_key?(key) accept_keys = IRB.conf.fetch(ACCEPT_KEYS_CONFIG, DEFAULT_ACCEPT_KEYS) accept_keys.any? { |k| key_match?(key, k) } end |
#accept_matching_suggestion? ⇒ Boolean
Accepts the current suggestion when it extends the buffer.
52 53 54 55 56 57 58 59 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 52 def accept_matching_suggestion? buffer = whole_buffer suggestion = find_suggestion(buffer) return false unless suggestion && suggestion != buffer accept_suggestion(suggestion) true end |
#accept_suggestion(suggestion) ⇒ void
This method returns an undefined value.
Replaces the entire buffer with the accepted suggestion and triggers a rerender.
463 464 465 466 467 468 469 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 463 def accept_suggestion(suggestion) sug_lines = suggestion.split("\n") @buffer_of_lines = sug_lines @line_index = sug_lines.size - 1 @byte_pointer = sug_lines.last.bytesize rerender end |
#colorize_ghost_lines(ghost, suggestion, ghost_byte_start = nil) ⇒ Array<String>
Colorizes the full suggestion and extracts the ghost portion.
ghost_byte_start is the visible-byte offset of the ghost within
the suggestion. It must come from the full (untruncated) ghost:
using a truncated ghost shifts the offset into the middle of the
suggestion and the colored suffix no longer matches the buffer.
Colored lines are then truncated to fit the terminal width, same
as the plain-text ghost.
571 572 573 574 575 576 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 571 def colorize_ghost_lines(ghost, suggestion, ghost_byte_start = nil) colored = IRB::Color.colorize_code(suggestion) start = ghost_byte_start || (suggestion.bytesize - ghost.bytesize) colored_ghost = extract_ansi_colored_suffix(colored, start) truncate_colored_ghost(colored_ghost).map { |line| dim_line(line) } end |
#dim_line(line) ⇒ String
Prepends each ANSI foreground color code with 2; (dim)
and strips non-color attributes (bold, underline, reverse…).
Inner full resets are replaced with RESET_COLOR so dim
stays active across token boundaries.
641 642 643 644 645 646 647 648 649 650 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 641 def dim_line(line) inner = line.gsub(/\e\[(\d+(?:;\d+)*)m/) do params = Regexp.last_match(1) next RESET_COLOR if params == '0' color = params.split(';').map(&:to_i).select { |p| FG_COLORS.include?(p) } color.empty? ? '' : "\e[2;#{color.join(';')}m" end "#{DIM}#{inner}#{RESET}" end |
#enabled? ⇒ Boolean
Checks whether autosuggestions are enabled via IRB.conf or env var.
231 232 233 234 235 236 237 238 239 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 231 def enabled? case ENV.fetch(ENV_KEY, nil) when '0' then false when '1' then true else val = IRB.conf[CONFIG_KEY] val.nil? || val end end |
#extract_ansi_colored_suffix(colored_text, visible_byte_offset) ⇒ String
Extracts the suffix of an ANSI-colored string starting at a given visible byte offset, preserving all ANSI codes.
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 659 def extract_ansi_colored_suffix(colored_text, visible_byte_offset) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize pos = 0 visible = 0 pending_code = nil while visible < visible_byte_offset && pos < colored_text.length if colored_text[pos] == "\e" code_start = pos pos = colored_text.index('m', pos)&.succ || colored_text.length code = colored_text[code_start...pos] pending_code = [RESET, "\e[m"].include?(code) ? nil : code else visible += 1 pos += 1 end end suffix = colored_text[pos..] || String.new pending_code ? "#{pending_code}#{suffix}" : suffix end |
#find_next_match(buffer, from_pointer) ⇒ Integer?
Finds the index of the next (newer) history entry starting with buffer.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 296 def find_next_match(buffer, from_pointer) # rubocop:disable Metrics/CyclomaticComplexity return nil unless from_pointer start_idx = from_pointer + 1 return nil if start_idx > Reline::HISTORY.size - 1 (start_idx...Reline::HISTORY.size).each do |i| entry = Reline::HISTORY[i] next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry)) return i if entry.start_with?(buffer) end nil end |
#find_prev_match(buffer, from_pointer) ⇒ Integer?
Finds the index of the previous (older) history entry starting with buffer.
277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 277 def find_prev_match(buffer, from_pointer) start_idx = (from_pointer || Reline::HISTORY.size) - 1 return nil if start_idx.negative? start_idx.downto(0) do |i| entry = Reline::HISTORY[i] next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry)) return i if entry.start_with?(buffer) end nil end |
#find_suggestion(buffer) ⇒ String?
Finds the most recent history entry that starts with the given buffer.
452 453 454 455 456 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 452 def find_suggestion(buffer) Reline::HISTORY.reverse.find do |h| h != buffer && h.start_with?(buffer) end end |
#ghost_color ⇒ String
Returns the effective ANSI color code for ghost text. Checks GHOST_STYLE_CONFIG first, then GHOST_COLOR_CONFIG, then default.
527 528 529 530 531 532 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 527 def ghost_color style = IRB.conf[GHOST_STYLE_CONFIG] return resolve_ghost_style(style) if style.is_a?(Hash) IRB.conf.fetch(GHOST_COLOR_CONFIG, DEFAULT_GHOST_COLOR) end |
#ghost_display_lines(ghost, suggestion, ghost_byte_start = nil) ⇒ Array<String>
Returns ghost lines ready for terminal output (with ANSI codes).
When colorization is enabled, the full suggestion is colorized via IRB::Color and the ghost portion is extracted from the colored output.
512 513 514 515 516 517 518 519 520 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 512 def ghost_display_lines(ghost, suggestion, ghost_byte_start = nil) if suggestion && use_colorize? colorize_ghost_lines(ghost, suggestion, ghost_byte_start) else ghost.split("\n").map { |line| "#{ghost_color}#{line}#{RESET}" } end rescue StandardError ghost.split("\n").map { |line| "#{ghost_color}#{line}#{RESET}" } end |
#input_key(key) ⇒ Object
Intercepts key input to accept autosuggestions on configured keys and clears the prefix navigation anchor on non-history keys.
37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 37 def input_key(key) # Some keys (Enter, submission) bypass rerender, so ghost is cleared # on every key to avoid stale ghost lines leftover on screen. clear_previous_ghost if enabled? clear_prefix_anchor if && !(key) return if enabled? && accept_key?(key) && accept_matching_suggestion? super end |
#key_match?(key, config_symbol) ⇒ Boolean
Checks if a key matches a given config symbol.
259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 259 def key_match?(key, config_symbol) return false unless key.respond_to?(:method_symbol) if config_symbol == :tab key.method_symbol == :ed_insert && key.respond_to?(:char) && key.char == "\t" else key.method_symbol == config_symbol end end |
#navigation_enabled? ⇒ Boolean
Whether prefix-filtered history navigation is enabled.
Falls back to the value of enabled? when not explicitly set.
217 218 219 220 221 222 223 224 225 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 217 def case ENV.fetch(ENV_NAV_KEY, nil) when '0' then false when '1' then true else val = IRB.conf[CONFIG_NAV_KEY] val.nil? ? enabled? : val end end |
#render ⇒ Object
27 |
# File 'sig/lib/irb/autosuggestions/line_editor_patch.rbs', line 27
def render: (*untyped) -> untyped
|
#render_ghost(ghost, suggestion = nil, ghost_byte_start = nil) ⇒ void
This method returns an undefined value.
Writes the ghost text (inline + extra lines) to terminal output. Saves and restores cursor column so Reline's cursor tracking (used for left/right arrow positioning) is not disturbed.
If suggestion is provided and colorization is enabled, the ghost
is rendered with syntax highlighting via IRB::Color.
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 484 def render_ghost(ghost, suggestion = nil, ghost_byte_start = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize output = Reline.core.instance_variable_get(:@output) display_lines = ghost_display_lines(ghost, suggestion, ghost_byte_start) @ghost_line_count = display_lines.size - 1 @has_inline_ghost = true output.write(move_to_buffer_end) first_line = display_lines.first output.write(first_line) if first_line && !first_line.empty? write_extra_ghost_lines(display_lines.drop(1)) origin_column = cursor_column_at_byte_pointer output.write("\e[#{@ghost_line_count}A") if @ghost_line_count.positive? output.write("\e[0G\e[#{origin_column}C") output.flush end |
#render_ghost_suggestion ⇒ void
This method returns an undefined value.
Renders ghost text for the current buffer, if a suggestion exists.
The ghost is reduced to fit the terminal width so it never wraps onto the following row. A ghost that wraps corrupts Reline's single-row cursor model (duplicated text, stale ANSI state), because Reline does not track visual wrapping of long lines. Suggestions remain accept-able via the configured keys even when the ghost is truncated.
341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 341 def render_ghost_suggestion buffer = whole_buffer @ghost_line_count = 0 return if buffer.empty? suggestion = find_suggestion(buffer) return unless suggestion ghost = suggestion[buffer.size..] return if ghost.nil? || ghost.empty? render_ghost(truncate_ghost_to_terminal(ghost), suggestion, suggestion.bytesize - ghost.bytesize) end |
#rerender ⇒ Object
Injects ghost text into terminal output after Reline finishes rendering.
Must be public because Reline::Core calls rerender externally.
Works on both Reline 0.3.x (rerender is the main rendering entry) and
Reline 0.4+ (rerender calls render internally).
Ghost is cleared BEFORE super to avoid cursor-position-dependent escapes targeting wrong lines after accept_suggestion changes buffer.
70 71 72 73 74 75 76 77 78 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 70 def rerender clear_previous_ghost if enabled? result = super render_ghost_suggestion if enabled? result end |
#resolve_ghost_style(hash) ⇒ String
Converts a style hash like { fg: :bright_black, italic: true }
to an ANSI escape sequence.
540 541 542 543 544 545 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 540 def resolve_ghost_style(hash) codes = [] codes << FG_MAP[hash[:fg]] if hash[:fg] ATTR_MAP.each { |attr, code| codes << code if hash[attr] } "\e[#{codes.join(';')}m" end |
#use_colorize? ⇒ Boolean
Checks whether syntax coloring is available and enabled.
551 552 553 554 555 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 551 def use_colorize? defined?(IRB::Color) && IRB::Color.colorable? && IRB.conf.fetch(:USE_COLORIZE, true) end |
#write_extra_ghost_lines(lines) ⇒ void
This method returns an undefined value.
Writes extra ghost lines below the current buffer line with prompt-width alignment.
686 687 688 689 690 691 692 693 694 695 696 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 686 def write_extra_ghost_lines(lines) return if lines.empty? output = Reline.core.instance_variable_get(:@output) lines.each do |line| output.write("\n\e[K") output.write("\e[#{prompt_width}C") if prompt_width.positive? output.write(line) end end |