diff --git a/lib/reline.rb b/lib/reline.rb index 03e4b745cf..9112f4e058 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -247,14 +247,14 @@ def get_screen_size } # :nodoc: Reline::DEFAULT_DIALOG_CONTEXT = Array.new # :nodoc: - def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination) + def readmultiline(_prompt = '', _add_hist = false, prompt: _prompt, add_hist: _add_hist, rprompt: nil, &confirm_multiline_termination) @mutex.synchronize do unless confirm_multiline_termination raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') end io_gate.with_raw_input do - inner_readline(prompt, add_hist, true, &confirm_multiline_termination) + inner_readline(prompt, add_hist, true, rprompt: rprompt, &confirm_multiline_termination) end whole_buffer = line_editor.whole_buffer.dup @@ -273,10 +273,10 @@ def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination) end end - def readline(prompt = '', add_hist = false) + def readline(_prompt = '', _add_hist = false, prompt: _prompt, add_hist: _add_hist, rprompt: nil) @mutex.synchronize do io_gate.with_raw_input do - inner_readline(prompt, add_hist, false) + inner_readline(prompt, add_hist, false, rprompt: rprompt) end line = line_editor.line.dup @@ -290,7 +290,7 @@ def readline(prompt = '', add_hist = false) end end - private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination) + private def inner_readline(prompt, add_hist, multiline, rprompt: nil, &confirm_multiline_termination) if ENV['RELINE_STDERR_TTY'] if io_gate.win? $stderr = File.open(ENV['RELINE_STDERR_TTY'], 'a') @@ -323,6 +323,7 @@ def readline(prompt = '', add_hist = false) line_editor.prompt_proc = prompt_proc line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc + line_editor.rprompt = rprompt&.encode(encoding) pre_input_hook&.call diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index bfffd17d59..ab60979873 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -13,6 +13,7 @@ class Reline::LineEditor attr_accessor :prompt_proc attr_accessor :auto_indent_proc attr_accessor :dig_perfect_match_proc + attr_accessor :rprompt VI_MOTIONS = %i{ ed_prev_char @@ -476,6 +477,20 @@ def render prompt_width = Reline::Unicode.calculate_width(prompt, true) [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]] end + + # Add rprompt to the first visible line if set and there's room + if @rprompt && !@rprompt.empty? && new_lines[0] + rprompt_width = Reline::Unicode.calculate_width(@rprompt, true) + right_col = screen_width - rprompt_width + first_line = new_lines[0] + # Calculate the end of the current content (prompt + input) + content_end = first_line.sum { |_, width, _| width } + # Only show rprompt if there's at least 1 char gap between content and rprompt + if right_col > content_end + first_line << [right_col, rprompt_width, @rprompt] + end + end + if @menu_info @menu_info.lines(screen_width).each do |item| new_lines << [[0, Reline::Unicode.calculate_width(item), item]] @@ -491,8 +506,8 @@ def render next if row < 0 || row >= screen_height dialog_rows = new_lines[row] ||= [] - # index 0 is for prompt, index 1 is for line, index 2.. is for dialog - dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] + # index 0 is for prompt, index 1 is for line, index 2 is for rprompt, index 3.. is for dialog + dialog_rows[index + 3] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] end end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index aa0fd7d29a..6673188980 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -232,6 +232,42 @@ def test_pre_input_hook assert_equal(l, Reline.pre_input_hook) end + def test_readline_with_rprompt + pend if win? + lib = File.expand_path("../../lib", __dir__) + code = "p result: Reline.readline('>', rprompt: '[TIME]')" + out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io| + io.write "a\n" + io.close_write + io.read + end + assert_include(out, { result: 'a' }.inspect) + end + + def test_readline_with_keyword_arguments + pend if win? + lib = File.expand_path("../../lib", __dir__) + code = "p result: Reline.readline(prompt: '>', add_hist: true, rprompt: '[TIME]')" + out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io| + io.write "a\n" + io.close_write + io.read + end + assert_include(out, { result: 'a' }.inspect) + end + + def test_readmultiline_with_keyword_arguments + pend if win? + lib = File.expand_path("../../lib", __dir__) + code = "p result: Reline.readmultiline(prompt: '>', add_hist: true, rprompt: '[TIME]') { true }" + out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io| + io.write "a\n" + io.close_write + io.read + end + assert_include(out, { result: 'a' }.inspect) + end + def test_dig_perfect_match_proc assert_equal(nil, Reline.dig_perfect_match_proc) diff --git a/test/reline/yamatanooroti/multiline_repl b/test/reline/yamatanooroti/multiline_repl index 4930f2e9d8..bcab36b3d4 100755 --- a/test/reline/yamatanooroti/multiline_repl +++ b/test/reline/yamatanooroti/multiline_repl @@ -212,6 +212,10 @@ opt.on('--autocomplete-width-long') { }.select{ |c| c.start_with?(target) } } } +rprompt = nil +opt.on('--rprompt VAL') { |v| + rprompt = v +} opt.parse!(ARGV) begin @@ -222,7 +226,7 @@ end begin prompt = ENV['RELINE_TEST_PROMPT'] || "\e[1mprompt>\e[m " puts 'Multiline REPL.' - while code = Reline.readmultiline(prompt, true) { |code| TerminationChecker.terminated?(code) } + while code = Reline.readmultiline(prompt, true, rprompt: rprompt) { |code| TerminationChecker.terminated?(code) } case code.chomp when 'exit', 'quit', 'q' exit 0 diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 008ff4a5e2..a996b9ddd1 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -183,6 +183,35 @@ def test_prompt close end + def test_rprompt + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + assert_screen(<<~EOC) + Multiline REPL. + prompt> [RPROMPT] + EOC + close + end + + def test_rprompt_with_input + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + write("hello") + assert_screen(<<~EOC) + Multiline REPL. + prompt> hello [RPROMPT] + EOC + close + end + + def test_rprompt_hides_when_input_reaches_rprompt + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + write("a" * 30) + assert_screen(<<~EOC) + Multiline REPL. + prompt> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + EOC + close + end + def test_mode_string_emacs write_inputrc <<~LINES set show-mode-in-prompt on