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_suggestion(suggestion) ⇒ void
Replaces the entire buffer with the accepted suggestion and triggers a rerender.
-
#colorize_ghost_lines(ghost, suggestion) ⇒ 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) ⇒ 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) ⇒ 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".
240 241 242 243 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 240 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_suggestion(suggestion) ⇒ void
This method returns an undefined value.
Replaces the entire buffer with the accepted suggestion and triggers a rerender.
356 357 358 359 360 361 362 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 356 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) ⇒ Array<String>
Colorizes the full suggestion and extracts the ghost portion.
452 453 454 455 456 457 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 452 def colorize_ghost_lines(ghost, suggestion) colored = IRB::Color.colorize_code(suggestion) ghost_byte_start = suggestion.bytesize - ghost.bytesize colored_ghost = extract_ansi_colored_suffix(colored, ghost_byte_start) colored_ghost.split("\n").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.
467 468 469 470 471 472 473 474 475 476 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 467 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.
223 224 225 226 227 228 229 230 231 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 223 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.
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 485 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.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 288 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.
269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 269 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.
345 346 347 348 349 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 345 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.
416 417 418 419 420 421 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 416 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) ⇒ 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.
401 402 403 404 405 406 407 408 409 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 401 def ghost_display_lines(ghost, suggestion) if suggestion && use_colorize? colorize_ghost_lines(ghost, suggestion) 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 48 49 50 51 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 37 def input_key(key) clear_prefix_anchor if && !(key) if enabled? && accept_key?(key) buffer = whole_buffer suggestion = find_suggestion(buffer) if suggestion && suggestion != buffer accept_suggestion(suggestion) return end end super end |
#key_match?(key, config_symbol) ⇒ Boolean
Checks if a key matches a given config symbol.
251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 251 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.
209 210 211 212 213 214 215 216 217 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 209 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) ⇒ 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.
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 375 def render_ghost(ghost, suggestion = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize output = Reline.core.instance_variable_get(:@output) display_lines = ghost_display_lines(ghost, suggestion) @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.
326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 326 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(ghost, suggestion) 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.
62 63 64 65 66 67 68 69 70 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 62 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.
429 430 431 432 433 434 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 429 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.
440 441 442 443 444 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 440 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.
512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/irb/autosuggestions/line_editor_patch.rb', line 512 def write_extra_ghost_lines(lines) return if lines.empty? prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0 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 |