diff --git a/lib/reline.rb b/lib/reline.rb index b233a85838..d079e13c9a 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -2,6 +2,7 @@ require 'reline/version' require 'reline/config' require 'reline/key_actor' +require 'reline/key_stroke' require 'reline/line_editor' module Reline @@ -85,11 +86,24 @@ def self.readline(prompt = '', add_hist = false) @line_editor.completion_proc = @completion_proc @line_editor.retrieve_completion_block = method(:retrieve_completion_block) @line_editor.rerender + config = { + key_mapping: { + # TODO + # "a" => "bb", + # "z" => "aa", + # "y" => "ak", + } + } + key_stroke = Reline::KeyStroke.new(config) begin while c = getc - @line_editor.input_key(c) - @line_editor.rerender - break if @line_editor.finished? + key_stroke.input_to!(c)&.then { |inputs| + inputs.each { |c| + @line_editor.input_key(c) + @line_editor.rerender + break if @line_editor.finished? + } + } end move_cursor_column(0) if add_hist and @line_editor.line and @line_editor.line.chomp.size > 0 diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb new file mode 100644 index 0000000000..2fec45b667 --- /dev/null +++ b/lib/reline/key_stroke.rb @@ -0,0 +1,70 @@ +class Reline::KeyStroke + using Module.new { + refine Array do + def start_with?(other) + other.size <= size && other == self.take(other.size) + end + end + } + + def initialize(config) + @config = config + @buffer = [] + end + + def input_to(bytes) + case match_status(bytes) + when :matching + nil + when :matched + expand(bytes) + when :unmatched + bytes + end + end + + def input_to!(bytes) + @buffer.concat Array(bytes) + input_to(@buffer)&.tap { clear } + end + + private + + def match_status(input) + key_mapping.keys.select { |lhs| + lhs.start_with? input + }.tap { |it| + return :matched if it.size == 1 && (it.max_by(&:size)&.size&.== input.size) + return :matching if it.size == 1 && (it.max_by(&:size)&.size&.!= input.size) + return :matched if it.max_by(&:size)&.size&.< input.size + return :matching if it.size > 1 + } + key_mapping.keys.select { |lhs| + input.start_with? lhs + }.tap { |it| + return it.size > 0 ? :matched : :unmatched + } + end + + def expand(input) + lhs = key_mapping.keys.select { |lhs| input.start_with? lhs }.sort_by(&:size).reverse.first + return input unless lhs + rhs = key_mapping[lhs] + + case rhs + when String + rhs_bytes = rhs.bytes + expand(expand(rhs_bytes) + expand(input.drop(lhs.size))) + when Symbol + [rhs] + expand(input.drop(lhs.size)) + end + end + + def key_mapping + @config[:key_mapping].transform_keys(&:bytes) + end + + def clear + @buffer = [] + end +end diff --git a/test/key_stroke_test.rb b/test/key_stroke_test.rb new file mode 100644 index 0000000000..65d24cadb1 --- /dev/null +++ b/test/key_stroke_test.rb @@ -0,0 +1,51 @@ +require 'helper' + +class Reline::KeyStroke::Test < Reline::TestCase + using Module.new { + refine Array do + def as_s + map(&:chr).join + end + end + } + + def test_input_to! + config = { + key_mapping: { + "a" => "xx", + "ab" => "y", + "abc" => "z", + "x" => "rr" + } + } + stroke = Reline::KeyStroke.new(config) + result = ("abzwabk".bytes).map { |char| + stroke.input_to!(char)&.then { |result| + "#{result.as_s}" + } + } + assert_equal(result, [nil, nil, "yz", "w", nil, nil, "yk"]) + end + + def test_input_to + config = { + key_mapping: { + "a" => "xx", + "ab" => "y", + "abc" => "z", + "x" => "rr" + } + } + stroke = Reline::KeyStroke.new(config) + assert_equal(stroke.input_to("a".bytes)&.as_s, nil) + assert_equal(stroke.input_to("ab".bytes)&.as_s, nil) + assert_equal(stroke.input_to("abc".bytes)&.as_s, "z") + assert_equal(stroke.input_to("abz".bytes)&.as_s, "yz") + assert_equal(stroke.input_to("abx".bytes)&.as_s, "yrr") + assert_equal(stroke.input_to("ac".bytes)&.as_s, "rrrrc") + assert_equal(stroke.input_to("aa".bytes)&.as_s, "rrrrrrrr") + assert_equal(stroke.input_to("x".bytes)&.as_s, "rr") + assert_equal(stroke.input_to("m".bytes)&.as_s, "m") + assert_equal(stroke.input_to("abzwabk".bytes)&.as_s, "yzwabk") + end +end