Skip to content

Commit

Permalink
Unite key bindings, key mapping and multibyte buffer
Browse files Browse the repository at this point in the history
  • Loading branch information
tompng committed May 26, 2024
1 parent d60f1e1 commit 3a68bf2
Show file tree
Hide file tree
Showing 19 changed files with 291 additions and 437 deletions.
147 changes: 42 additions & 105 deletions lib/reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,9 @@ module Reline

class ConfigEncodingConversionError < StandardError; end

Key = Struct.new(:char, :combined_char, :with_meta) do
def match?(other)
case other
when Reline::Key
(other.char.nil? or char.nil? or char == other.char) and
(other.combined_char.nil? or combined_char.nil? or combined_char == other.combined_char) and
(other.with_meta.nil? or with_meta.nil? or with_meta == other.with_meta)
when Integer, Symbol
(combined_char and combined_char == other) or
(combined_char.nil? and char and char == other)
else
false
end
Key = Struct.new(:char, :combined_char, :bytes) do
def match?(sym)
combined_char == sym
end
alias_method :==, :match?
end
Expand Down Expand Up @@ -349,24 +339,22 @@ def readline(prompt = '', add_hist = false)
begin
line_editor.set_signal_handlers
loop do
read_io(config.keyseq_timeout) { |inputs|
line_editor.set_pasting_state(io_gate.in_pasting?)
inputs.each do |key|
if key.char == :bracketed_paste_start
text = io_gate.read_bracketed_paste
line_editor.insert_pasted_text(text)
line_editor.scroll_into_view
else
line_editor.update(key)
end
end
wait_occurs = false
key = read_io(config.keyseq_timeout) {
line_editor.set_pasting_state(false)
wait_occurs = true
line_editor.rerender
}
line_editor.set_pasting_state(!wait_occurs && io_gate.in_pasting?)
if key.combined_char == :bracketed_paste_start
line_editor.insert_pasted_text(io_gate.read_bracketed_paste)
line_editor.scroll_into_view
else
line_editor.update(key)
end
if line_editor.finished?
line_editor.render_finished
break
else
line_editor.set_pasting_state(io_gate.in_pasting?)
line_editor.rerender
end
end
io_gate.move_cursor_column(0)
Expand All @@ -378,92 +366,41 @@ def readline(prompt = '', add_hist = false)
end
end

# GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC
# is followed by a character, and times out and treats it as a standalone
# ESC if the second character does not arrive. If the second character
# comes before timed out, it is treated as a modifier key with the
# meta-property of meta-key, so that it can be distinguished from
# multibyte characters with the 8th bit turned on.
#
# GNU Readline will wait for the 2nd character with "keyseq-timeout"
# milli-seconds but wait forever after 3rd characters.
# GNU Readline waits for "keyseq-timeout" milliseconds when the input is ambiguous whether it is matching or matched.
# For example, ESC can be a standalone ESC or part of a sequence that starts with ESC.
# GNU Readline also waits for any other amibugous keybindings defined in inputrc.
private def read_io(keyseq_timeout, &block)
buffer = []
prev_status = :matching
loop do
c = io_gate.getc(Float::INFINITY)
if c == -1
result = :unmatched
timeout = prev_status == :matching_matched ? keyseq_timeout.fdiv(1000) : Float::INFINITY
block.call unless io_gate.in_pasting?
c = io_gate.getc(timeout)
if c == -1 || c.nil?
if prev_status != :matching_matched
# Input closed
return Reline::Key.new(nil, nil, [])
end
status = :matched
else
buffer << c
result = key_stroke.match_status(buffer)
status = key_stroke.match_status(buffer)
end
case result
when :matched
expanded = key_stroke.expand(buffer).map{ |expanded_c|
Reline::Key.new(expanded_c, expanded_c, false)
}
block.(expanded)
break
when :matching
if buffer.size == 1
case read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
when :break then break
when :next then next
end
if status == :matched || (prev_status == :matching_matched && status == :unmatched)
expanded, bytes, rest = key_stroke.expand(buffer)
if expanded.is_a?(Array)
rest = expanded + rest
end
when :unmatched
if buffer.size == 1 and c == "\e".ord
read_escaped_key(keyseq_timeout, c, block)
else
expanded = buffer.map{ |expanded_c|
Reline::Key.new(expanded_c, expanded_c, false)
}
block.(expanded)
rest.reverse_each { |c| io_gate.ungetc(c) }
buffer = []
case expanded
when Symbol
return Reline::Key.new(bytes.last, expanded, bytes)
when String
return Reline::Key.new(expanded, :ed_insert, bytes)
end
break
end
end
end

private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
succ_c = io_gate.getc(keyseq_timeout.fdiv(1000))
if succ_c
case key_stroke.match_status(buffer.dup.push(succ_c))
when :unmatched
if c == "\e".ord
block.([Reline::Key.new(succ_c, succ_c | 0b10000000, true)])
else
block.([Reline::Key.new(c, c, false), Reline::Key.new(succ_c, succ_c, false)])
end
return :break
when :matching
io_gate.ungetc(succ_c)
return :next
when :matched
buffer << succ_c
expanded = key_stroke.expand(buffer).map{ |expanded_c|
Reline::Key.new(expanded_c, expanded_c, false)
}
block.(expanded)
return :break
end
else
block.([Reline::Key.new(c, c, false)])
return :break
end
end

private def read_escaped_key(keyseq_timeout, c, block)
escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000))

if escaped_c.nil?
block.([Reline::Key.new(c, c, false)])
elsif escaped_c >= 128 # maybe, first byte of multi byte
block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)])
elsif escaped_c == "\e".ord # escape twice
block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)])
else
block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)])
prev_status = status
end
end

Expand Down Expand Up @@ -547,7 +484,7 @@ def self.encoding_system_needs
def self.core
@core ||= Core.new { |core|
core.config = Reline::Config.new
core.key_stroke = Reline::KeyStroke.new(core.config)
core.key_stroke = Reline::KeyStroke.new(core.config, core.encoding)
core.line_editor = Reline::LineEditor.new(core.config, core.encoding)

core.basic_word_break_characters = " \t\n`><=;|&{("
Expand Down
39 changes: 19 additions & 20 deletions lib/reline/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@ class InvalidInputrc < RuntimeError

def initialize
@additional_key_bindings = {} # from inputrc
@additional_key_bindings[:emacs] = {}
@additional_key_bindings[:vi_insert] = {}
@additional_key_bindings[:vi_command] = {}
@oneshot_key_bindings = {}
@additional_key_bindings[:emacs] = Reline::KeyActor::Base.new
@additional_key_bindings[:vi_insert] = Reline::KeyActor::Base.new
@additional_key_bindings[:vi_command] = Reline::KeyActor::Base.new
@oneshot_key_bindings = Reline::KeyActor::Base.new
@editing_mode_label = :emacs
@keymap_label = :emacs
@keymap_prefix = []
@key_actors = {}
@key_actors[:emacs] = Reline::KeyActor::Emacs.new
@key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new
@key_actors[:vi_command] = Reline::KeyActor::ViCommand.new
@key_actors[:emacs] = Reline::KeyActor::Base.new
@key_actors[:vi_insert] = Reline::KeyActor::Base.new
@key_actors[:vi_command] = Reline::KeyActor::Base.new
@key_actors[:emacs].add_mappings(Reline::KeyActor::EMACS_MAPPING)
@key_actors[:vi_insert].add_mappings(Reline::KeyActor::VI_INSERT_MAPPING)
@key_actors[:vi_command].add_mappings(Reline::KeyActor::VI_COMMAND_MAPPING)
@vi_cmd_mode_string = '(cmd)'
@vi_ins_mode_string = '(ins)'
@emacs_mode_string = '@'
Expand All @@ -62,7 +65,7 @@ def reset
end

def editing_mode
@key_actors[@editing_mode_label]
@editing_mode_label
end

def editing_mode=(val)
Expand All @@ -73,10 +76,6 @@ def editing_mode_is?(*val)
val.any?(@editing_mode_label)
end

def keymap
@key_actors[@keymap_label]
end

def loaded?
@loaded
end
Expand Down Expand Up @@ -133,26 +132,26 @@ def read(file = nil)

def key_bindings
# The key bindings for each editing mode will be overwritten by the user-defined ones.
kb = @key_actors[@editing_mode_label].default_key_bindings.dup
kb.merge!(@additional_key_bindings[@editing_mode_label])
kb.merge!(@oneshot_key_bindings)
kb
Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @key_actors[@editing_mode_label]])
end

def add_oneshot_key_binding(keystroke, target)
@oneshot_key_bindings[keystroke] = target
# Old IRB sets invalid keystroke [Reline::Key]. We should ignore it.
return unless keystroke.all? { |c| c.is_a?(Integer) }

@oneshot_key_bindings.add(keystroke, target)
end

def reset_oneshot_key_bindings
@oneshot_key_bindings.clear
end

def add_default_key_binding_by_keymap(keymap, keystroke, target)
@key_actors[keymap].default_key_bindings[keystroke] = target
@key_actors[keymap].add(keystroke, target)
end

def add_default_key_binding(keystroke, target)
@key_actors[@keymap_label].default_key_bindings[keystroke] = target
@key_actors[@keymap_label].add(keystroke, target)
end

def read_lines(lines, file = nil)
Expand Down Expand Up @@ -192,7 +191,7 @@ def read_lines(lines, file = nil)
func_name = func_name.split.first
keystroke, func = bind_key(key, func_name)
next unless keystroke
@additional_key_bindings[@keymap_label][@keymap_prefix + keystroke] = func
@additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func)
end
end
unless if_stack.empty?
Expand Down
1 change: 1 addition & 0 deletions lib/reline/key_actor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Reline::KeyActor
end

require 'reline/key_actor/base'
require 'reline/key_actor/composite'
require 'reline/key_actor/emacs'
require 'reline/key_actor/vi_command'
require 'reline/key_actor/vi_insert'
35 changes: 28 additions & 7 deletions lib/reline/key_actor/base.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
class Reline::KeyActor::Base
MAPPING = Array.new(256)
def initialize
@matching_bytes = {}
@key_bindings = {}
end

def get_method(key)
self.class::MAPPING[key]
def add_mappings(mappings)
add([27], :ed_ignore)
128.times do |key|
func = mappings[key]
meta_func = mappings[key | 0b10000000]
add([key], func) unless func == :ed_unassigned
add([27, key], meta_func) unless meta_func == :ed_unassigned
end
end

def initialize
@default_key_bindings = {}
def add(key, func)
(1...key.size).each do |size|
@matching_bytes[key.take(size)] = true
end
@key_bindings[key] = func
end

def matching?(key)
@matching_bytes[key]
end

def get(key)
@key_bindings[key]
end

def default_key_bindings
@default_key_bindings
def clear
@matching_bytes.clear
@key_bindings.clear
end
end
17 changes: 17 additions & 0 deletions lib/reline/key_actor/composite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Reline::KeyActor::Composite
def initialize(key_actors)
@key_actors = key_actors
end

def matching?(key)
@key_actors.any? { |key_actor| key_actor.matching?(key) }
end

def get(key)
@key_actors.each do |key_actor|
func = key_actor.get(key)
return func if func
end
nil
end
end
4 changes: 2 additions & 2 deletions lib/reline/key_actor/emacs.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Reline::KeyActor::Emacs < Reline::KeyActor::Base
MAPPING = [
module Reline::KeyActor
EMACS_MAPPING = [
# 0 ^@
:em_set_mark,
# 1 ^A
Expand Down
4 changes: 2 additions & 2 deletions lib/reline/key_actor/vi_command.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
MAPPING = [
module Reline::KeyActor
VI_COMMAND_MAPPING = [
# 0 ^@
:ed_unassigned,
# 1 ^A
Expand Down
4 changes: 2 additions & 2 deletions lib/reline/key_actor/vi_insert.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
MAPPING = [
module Reline::KeyActor
VI_INSERT_MAPPING = [
# 0 ^@
:ed_unassigned,
# 1 ^A
Expand Down
Loading

0 comments on commit 3a68bf2

Please sign in to comment.