Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/reline_pac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
require_relative 'reline_pac/version'
require_relative 'reline_pac/packages'
require_relative 'reline_pac/config'
require_relative 'reline_pac/introspection'
# Load commands only if IRB is available
require_relative 'reline_pac/irb_commands' if defined?(IRB)
Comment on lines +8 to +9
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional require 'require_relative 'reline_pac/irb_commands' if defined?(IRB)' may not work as intended in all scenarios. The file itself requires 'irb' at the top (line 3 of irb_commands.rb), which means if IRB is not available when this line executes, it will raise a LoadError. The conditional should either wrap the require statement in the irb_commands.rb file or use a rescue block here to handle cases where IRB is not installed.

Suggested change
# Load commands only if IRB is available
require_relative 'reline_pac/irb_commands' if defined?(IRB)
# Load commands only if IRB is available, and fail gracefully if IRB cannot be loaded
begin
require_relative 'reline_pac/irb_commands' if defined?(IRB)
rescue LoadError
# IRB is not installed or could not be loaded; skip IRB-specific commands
end

Copilot uses AI. Check for mistakes.

# RelinePac provides Reline extensions for completion, history search, and clipboard helpers.
module RelinePac
Expand Down
90 changes: 90 additions & 0 deletions lib/reline_pac/introspection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

module RelinePac
# Introspection provides utilities to inspect and debug Reline keybindings.
module Introspection
class << self
# Display all current keybindings in a human-readable format.
def keymap
config = Reline.core.config
bindings = {}

config.keymap.instance_variable_get(:@key_bindings).each do |key, method|
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses instance_variable_get to access Reline's internal @key_bindings, which is a brittle approach that depends on internal implementation details. If Reline changes its internal structure in future versions, this code will break. Consider checking if Reline provides a public API for accessing keybindings, or add error handling for when the instance variable doesn't exist.

Suggested change
config.keymap.instance_variable_get(:@key_bindings).each do |key, method|
keymap = config.keymap
key_bindings =
if keymap.respond_to?(:key_bindings)
keymap.key_bindings
elsif keymap.instance_variable_defined?(:@key_bindings)
keymap.instance_variable_get(:@key_bindings)
else
{}
end
key_bindings.each do |key, method|

Copilot uses AI. Check for mistakes.
# Skip ed_insert as it's used for regular character input
next if %i[ed_insert ed_digit].include?(method)

key_str = format_key_sequence(key)

# Skip if we've already displayed this key
next if bindings[key_str]

bindings[key_str] = method
end
bindings
end

private

# Format a key sequence (byte array) into a readable string.
# @param key [Array<Integer>] array of bytes representing the key sequence
# @return [String] formatted key representation
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
def format_key_sequence(key)
sequence = key.pack('C*')

case sequence
when "\e[A", "\eOA" then '↑'
when "\e[B", "\eOB" then '↓'
when "\e[C", "\eOC" then '→'
when "\e[D", "\eOD" then '←'
when "\e[F", "\eOF" then 'End'
when "\e[H", "\eOH" then 'Home'
when "\e[1~", "\e[7~" then 'Home' # rubocop:disable Lint/DuplicateBranch
when "\e[4~", "\e[8~" then 'End' # rubocop:disable Lint/DuplicateBranch
Comment on lines +40 to +43
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rubocop:disable comments for Lint/DuplicateBranch on lines 42 and 43 are suppressing legitimate warnings. These case branches do have duplicate 'Home' and 'End' return values. The duplicate branches could be combined (e.g., when "\e[F", "\eOF", "\e[4~", "\e[8~" then 'End') to eliminate the need for these suppressions and improve maintainability.

Suggested change
when "\e[F", "\eOF" then 'End'
when "\e[H", "\eOH" then 'Home'
when "\e[1~", "\e[7~" then 'Home' # rubocop:disable Lint/DuplicateBranch
when "\e[4~", "\e[8~" then 'End' # rubocop:disable Lint/DuplicateBranch
when "\e[F", "\eOF", "\e[4~", "\e[8~" then 'End'
when "\e[H", "\eOH", "\e[1~", "\e[7~" then 'Home'

Copilot uses AI. Check for mistakes.
when "\e[3~" then 'Delete'
when "\e[5~" then 'PageUp'
when "\e[6~" then 'PageDown'
when "\e[Z" then '⇧Tab'
when "\e[200~" then 'BracketedPaste'
when /\e\[1;5([ABCD])/ then "⌃#{arrow_symbol(::Regexp.last_match(1))}"
when /\e\[1;3([ABCD])/ then "⌥#{arrow_symbol(::Regexp.last_match(1))}"
when "\e\x1B[C" then '⌃→'
when "\e\x1B[D" then '⌃←'
else
if key.first == 27 && key.size > 1
rest = key[1..].map { |byte| format_byte(byte) }.join
"⎋#{rest}"
else
key.map { |byte| format_byte(byte) }.join
end
end
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

# Convert arrow key letter to symbol.
# @param letter [String] 'A', 'B', 'C', or 'D'
# @return [String] arrow symbol
def arrow_symbol(letter)
{ 'A' => '↑', 'B' => '↓', 'C' => '→', 'D' => '←' }[letter]
end

# Format a single byte into a readable representation.
# @param byte [Integer] byte value (0-255)
# @return [String] formatted byte representation
def format_byte(byte)
case byte
when 27
'⎋'
when 0..31 # Control キー
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 78 contains Japanese text ('Control キー' meaning 'Control key'). For consistency with the rest of the codebase where English is used, this comment should be written in English.

Suggested change
when 0..31 # Control キー
when 0..31 # Control key

Copilot uses AI. Check for mistakes.
"⌃#{(byte + 64).chr}"
when 32
'␣'
when 127
'⌫'
else
byte.chr.match?(/[[:print:]]/) ? byte.chr : format('\\x%02X', byte)
end
end
end
end
end
25 changes: 25 additions & 0 deletions lib/reline_pac/irb_commands.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require 'irb'

module RelinePac
module IrbCommands
# IRB command to display Reline keybindings
class Keybinds < ::IRB::Command::Base
category 'Context'
description 'Show Reline keybindings'

def execute(_arg)
bindings = RelinePac::Introspection.keymap
max_key_length = bindings.keys.map(&:length).max
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The execute method doesn't handle the case where bindings is empty or max_key_length is nil. If keymap returns an empty hash, bindings.keys.map(&:length).max will return nil, and calling ljust on a string with nil as an argument will raise a TypeError.

Suggested change
max_key_length = bindings.keys.map(&:length).max
max_key_length = bindings.keys.map(&:length).max || 0

Copilot uses AI. Check for mistakes.

bindings.each do |key, method|
puts "#{key.ljust(max_key_length + 2)} #{method}"
end
end
end
Comment on lines +8 to +20
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test cases don't cover the IrbCommands::Keybinds class, which means the execute method and command registration logic lacks test coverage. Since other modules in the repo have corresponding test files, this command should also have tests.

Copilot uses AI. Check for mistakes.
end
end

# IRB が読み込まれている場合のみ登録
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 24 is in Japanese while all other code and comments in the file are in English. For consistency and to ensure all team members can understand the codebase, comments should be written in English.

Suggested change
# IRB が読み込まれている場合のみ登録
# Register only if IRB has been loaded

Copilot uses AI. Check for mistakes.
IRB::Command.register(:show_keybinds, RelinePac::IrbCommands::Keybinds) if defined?(IRB::Command)
41 changes: 41 additions & 0 deletions spec/reline_pac/introspection_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe RelinePac::Introspection do
describe '.keymap' do
it 'returns a hash without errors' do
expect { described_class.keymap }.not_to raise_error
end

it 'returns a hash of keybindings' do
result = described_class.keymap
expect(result).to be_a(Hash)
end

it 'filters out ed_insert and ed_digit bindings' do
result = described_class.keymap
expect(result.values).not_to include(:ed_insert)
expect(result.values).not_to include(:ed_digit)
end

it 'includes known keybindings' do
result = described_class.keymap
# Control-A should map to ed_move_to_beg
expect(result).to include('⌃A' => :ed_move_to_beg)
Comment on lines +22 to +25
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects Control-A (⌃A) to always map to ed_move_to_beg, but this assumption depends on the default Reline configuration and may vary across different environments or Reline versions. This could make the test brittle. Consider either mocking the keymap data or testing the formatting logic independently of specific keybinding values.

Suggested change
it 'includes known keybindings' do
result = described_class.keymap
# Control-A should map to ed_move_to_beg
expect(result).to include('⌃A' => :ed_move_to_beg)
it 'includes and formats movement keybindings when present' do
result = described_class.keymap
# If ed_move_to_beg is present, its key should be formatted as a Control key
key_for_move_to_beg = result.key(:ed_move_to_beg)
if key_for_move_to_beg
expect(key_for_move_to_beg).to include('⌃')
end

Copilot uses AI. Check for mistakes.
end

it 'removes duplicate keys' do
result = described_class.keymap
# All keys should be unique
expect(result.keys.uniq.length).to eq(result.keys.length)
end

it 'formats special keys correctly' do
result = described_class.keymap
# Should have Control and Escape keys formatted with symbols
expect(result.keys.grep(/⌃/).any?).to be true
expect(result.keys.grep(/⎋/).any?).to be true
end
end
end
Loading