Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unite key bindings, key mapping and multibyte buffer #708

Closed
wants to merge 1 commit into from
Closed
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
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, :method_symbol, :bytes) do
def match?(sym)
method_symbol == 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.method_symbol == :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 != :unmatched && 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
Loading