diff options
-rw-r--r-- | lib/reline.rb | 16 | ||||
-rw-r--r-- | lib/reline/ansi.rb | 39 | ||||
-rw-r--r-- | lib/reline/config.rb | 68 | ||||
-rw-r--r-- | lib/reline/history.rb | 42 | ||||
-rw-r--r-- | lib/reline/key_actor/emacs.rb | 12 | ||||
-rw-r--r-- | lib/reline/key_actor/vi_command.rb | 4 | ||||
-rw-r--r-- | lib/reline/key_actor/vi_insert.rb | 4 | ||||
-rw-r--r-- | lib/reline/key_stroke.rb | 2 | ||||
-rw-r--r-- | lib/reline/line_editor.rb | 249 | ||||
-rw-r--r-- | lib/reline/reline.gemspec | 2 | ||||
-rw-r--r-- | lib/reline/unicode.rb | 68 | ||||
-rw-r--r-- | lib/reline/version.rb | 2 | ||||
-rw-r--r-- | lib/reline/windows.rb | 15 | ||||
-rw-r--r-- | test/reline/test_config.rb | 107 | ||||
-rw-r--r-- | test/reline/test_history.rb | 20 | ||||
-rw-r--r-- | test/reline/test_key_actor_emacs.rb | 154 | ||||
-rw-r--r-- | test/reline/test_key_stroke.rb | 19 | ||||
-rw-r--r-- | test/reline/test_reline.rb | 38 | ||||
-rw-r--r-- | test/reline/test_within_pipe.rb | 13 | ||||
-rw-r--r-- | test/reline/yamatanooroti/test_rendering.rb | 193 |
20 files changed, 914 insertions, 153 deletions
diff --git a/lib/reline.rb b/lib/reline.rb index 1537ee7e69..eb18d0d075 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -7,6 +7,7 @@ require 'reline/key_actor' require 'reline/key_stroke' require 'reline/line_editor' require 'reline/history' +require 'rbconfig' module Reline FILENAME_COMPLETION_PROC = nil @@ -98,22 +99,22 @@ module Reline end def completion_proc=(p) - raise ArgumentError unless p.respond_to?(:call) + raise ArgumentError unless p.respond_to?(:call) or p.nil? @completion_proc = p end def output_modifier_proc=(p) - raise ArgumentError unless p.respond_to?(:call) + raise ArgumentError unless p.respond_to?(:call) or p.nil? @output_modifier_proc = p end def prompt_proc=(p) - raise ArgumentError unless p.respond_to?(:call) + raise ArgumentError unless p.respond_to?(:call) or p.nil? @prompt_proc = p end def auto_indent_proc=(p) - raise ArgumentError unless p.respond_to?(:call) + raise ArgumentError unless p.respond_to?(:call) or p.nil? @auto_indent_proc = p end @@ -122,7 +123,7 @@ module Reline end def dig_perfect_match_proc=(p) - raise ArgumentError unless p.respond_to?(:call) + raise ArgumentError unless p.respond_to?(:call) or p.nil? @dig_perfect_match_proc = p end @@ -222,7 +223,6 @@ module Reline line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc line_editor.pre_input_hook = pre_input_hook - line_editor.rerender unless config.test_mode config.read @@ -232,6 +232,8 @@ module Reline end end + line_editor.rerender + begin loop do read_io(config.keyseq_timeout) { |inputs| @@ -243,6 +245,8 @@ module Reline break if line_editor.finished? end Reline::IOGate.move_cursor_column(0) + rescue Errno::EIO + # Maybe the I/O has been closed. rescue StandardError => e line_editor.finalize Reline::IOGate.deprep(otio) diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb index 3ef02d6e7a..80fccd74f9 100644 --- a/lib/reline/ansi.rb +++ b/lib/reline/ansi.rb @@ -28,12 +28,22 @@ class Reline::ANSI [27, 71, 67] => :ed_next_char, # → [27, 71, 68] => :ed_prev_char, # ← + # urxvt / exoterm + [27, 91, 55, 126] => :ed_move_to_beg, # Home + [27, 91, 56, 126] => :ed_move_to_end, # End + # GNOME [27, 79, 72] => :ed_move_to_beg, # Home [27, 79, 70] => :ed_move_to_end, # End # Del is 0x08 # Arrow keys are the same of KDE + # iTerm2 + [27, 27, 91, 67] => :em_next_word, # Option+→ + [27, 27, 91, 68] => :ed_prev_word, # Option+← + [195, 166] => :em_next_word, # Option+f + [195, 162] => :ed_prev_word, # Option+b + # others [27, 32] => :em_set_mark, # M-<space> [24, 24] => :em_exchange_mark, # C-x C-x TODO also add Windows @@ -61,8 +71,13 @@ class Reline::ANSI unless @@buf.empty? return @@buf.shift end - c = @@input.raw(intr: true, &:getbyte) + until c = @@input.raw(intr: true, &:getbyte) + sleep 0.1 + end (c == 0x16 && @@input.raw(min: 0, tim: 0, &:getbyte)) || c + rescue Errno::EIO + # Maybe the I/O has been closed. + nil end def self.ungetc(c) @@ -105,10 +120,13 @@ class Reline::ANSI @@input.raw do |stdin| @@output << "\e[6n" @@output.flush - while (c = stdin.getc) != 'R' - res << c if c + loop do + c = stdin.getc + next if c.nil? + res << c + m = res.match(/\e\[(?<row>\d+);(?<column>\d+)R/) + break if m end - m = res.match(/\e\[(?<row>\d+);(?<column>\d+)/) (m.pre_match + m.post_match).chars.reverse_each do |ch| stdin.ungetc ch end @@ -116,9 +134,16 @@ class Reline::ANSI column = m[:column].to_i - 1 row = m[:row].to_i - 1 rescue Errno::ENOTTY - buf = @@output.pread(@@output.pos, 0) - row = buf.count("\n") - column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 + begin + buf = @@output.pread(@@output.pos, 0) + row = buf.count("\n") + column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 + rescue Errno::ESPIPE + # Just returns column 1 for ambiguous width because this I/O is not + # tty and can't seek. + row = 0 + column = 1 + end end Reline::CursorPos.new(column, row) end diff --git a/lib/reline/config.rb b/lib/reline/config.rb index 53b868fd2e..370d100414 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -1,10 +1,6 @@ -require 'pathname' - class Reline::Config attr_reader :test_mode - DEFAULT_PATH = '~/.inputrc' - KEYSEQ_PATTERN = /\\(?:C|Control)-[A-Za-z_]|\\(?:M|Meta)-[0-9A-Za-z_]|\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]|\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]|\\e|\\[\\\"\'abdfnrtv]|\\\d{1,3}|\\x\h{1,2}|./ class InvalidInputrc < RuntimeError @@ -37,6 +33,10 @@ class Reline::Config show-all-if-ambiguous show-all-if-unmodified visible-stats + show-mode-in-prompt + vi-cmd-mode-icon + vi-ins-mode-icon + emacs-mode-string } VARIABLE_NAME_SYMBOLS = VARIABLE_NAMES.map { |v| :"#{v.tr(?-, ?_)}" } VARIABLE_NAME_SYMBOLS.each do |v| @@ -54,7 +54,11 @@ class Reline::Config @key_actors[:emacs] = Reline::KeyActor::Emacs.new @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new - @history_size = 500 + @vi_cmd_mode_icon = '(cmd)' + @vi_ins_mode_icon = '(ins)' + @emacs_mode_string = '@' + # https://tiswww.case.edu/php/chet/readline/readline.html#IDX25 + @history_size = -1 # unlimited @keyseq_timeout = 500 @test_mode = false end @@ -86,14 +90,31 @@ class Reline::Config def inputrc_path case ENV['INPUTRC'] when nil, '' - DEFAULT_PATH else - ENV['INPUTRC'] + return File.expand_path(ENV['INPUTRC']) end + + # In the XDG Specification, if ~/.config/readline/inputrc exists, then + # ~/.inputrc should not be read, but for compatibility with GNU Readline, + # if ~/.inputrc exists, then it is given priority. + home_rc_path = File.expand_path('~/.inputrc') + return home_rc_path if File.exist?(home_rc_path) + + case path = ENV['XDG_CONFIG_HOME'] + when nil, '' + else + path = File.join(path, 'readline/inputrc') + return path if File.exist?(path) and path == File.expand_path(path) + end + + path = File.expand_path('~/.config/readline/inputrc') + return path if File.exist?(path) + + return home_rc_path end def read(file = nil) - file ||= File.expand_path(inputrc_path) + file ||= inputrc_path begin if file.respond_to?(:readlines) lines = file.readlines @@ -144,7 +165,7 @@ class Reline::Config case line when /^set +([^ ]+) +([^ ]+)/i - var, value = $1.downcase, $2.downcase + var, value = $1.downcase, $2 bind_variable(var, value) next when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o @@ -194,7 +215,11 @@ class Reline::Config def bind_variable(name, value) case name when 'history-size' - @history_size = value.to_i + begin + @history_size = Integer(value) + rescue ArgumentError + @history_size = 500 + end when 'bell-style' @bell_style = case value @@ -233,12 +258,35 @@ class Reline::Config end when 'keyseq-timeout' @keyseq_timeout = value.to_i + when 'show-mode-in-prompt' + case value + when 'off' + @show_mode_in_prompt = false + when 'on' + @show_mode_in_prompt = true + else + @show_mode_in_prompt = false + end + when 'vi-cmd-mode-string' + @vi_cmd_mode_icon = retrieve_string(value) + when 'vi-ins-mode-string' + @vi_ins_mode_icon = retrieve_string(value) + when 'emacs-mode-string' + @emacs_mode_string = retrieve_string(value) when *VARIABLE_NAMES then variable_name = :"@#{name.tr(?-, ?_)}" instance_variable_set(variable_name, value.nil? || value == '1' || value == 'on') end end + def retrieve_string(str) + if str =~ /\A"(.*)"\z/ + parse_keyseq($1).map(&:chr).join + else + parse_keyseq(str).map(&:chr).join + end + end + def bind_key(key, func_name) if key =~ /\A"(.*)"\z/ keyseq = parse_keyseq($1) diff --git a/lib/reline/history.rb b/lib/reline/history.rb index d95f1cebc3..7a1ed6b90b 100644 --- a/lib/reline/history.rb +++ b/lib/reline/history.rb @@ -29,27 +29,47 @@ class Reline::History < Array end def push(*val) - diff = size + val.size - @config.history_size - if diff > 0 - if diff <= size - shift(diff) - else - diff -= size - clear - val.shift(diff) + # If history_size is zero, all histories are dropped. + return self if @config.history_size.zero? + # If history_size is negative, history size is unlimited. + if @config.history_size.positive? + diff = size + val.size - @config.history_size + if diff > 0 + if diff <= size + shift(diff) + else + diff -= size + clear + val.shift(diff) + end end end - super(*(val.map{ |v| String.new(v, encoding: Reline.encoding_system_needs) })) + super(*(val.map{ |v| + String.new(v, encoding: Reline.encoding_system_needs) + })) end def <<(val) - shift if size + 1 > @config.history_size + # If history_size is zero, all histories are dropped. + return self if @config.history_size.zero? + # If history_size is negative, history size is unlimited. + if @config.history_size.positive? + shift if size + 1 > @config.history_size + end super(String.new(val, encoding: Reline.encoding_system_needs)) end private def check_index(index) index += size if index < 0 - raise RangeError.new("index=<#{index}>") if index < -@config.history_size or @config.history_size < index + if index < -2147483648 or 2147483647 < index + raise RangeError.new("integer #{index} too big to convert to `int'") + end + # If history_size is negative, history size is unlimited. + if @config.history_size.positive? + if index < -@config.history_size or @config.history_size < index + raise RangeError.new("index=<#{index}>") + end + end raise IndexError.new("index=<#{index}>") if index < 0 or size <= index index end diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb index 3886d17f22..1e51d4fa18 100644 --- a/lib/reline/key_actor/emacs.rb +++ b/lib/reline/key_actor/emacs.rb @@ -37,9 +37,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 17 ^Q :ed_quoted_insert, # 18 ^R - :ed_search_prev_history, + :vi_search_prev, # 19 ^S - :ed_search_next_history, + :vi_search_next, # 20 ^T :ed_transpose_chars, # 21 ^U @@ -413,11 +413,11 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 205 M-M :ed_unassigned, # 206 M-N - :ed_search_next_history, + :vi_search_next, # 207 M-O :ed_sequence_lead_in, # 208 M-P - :ed_search_prev_history, + :vi_search_prev, # 209 M-Q :ed_unassigned, # 210 M-R @@ -477,11 +477,11 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 237 M-m :ed_unassigned, # 238 M-n - :ed_search_next_history, + :vi_search_next, # 239 M-o :ed_unassigned, # 240 M-p - :ed_search_prev_history, + :vi_search_prev, # 241 M-q :ed_unassigned, # 242 M-r diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb index 865dfa2e00..54b4a60383 100644 --- a/lib/reline/key_actor/vi_command.rb +++ b/lib/reline/key_actor/vi_command.rb @@ -37,7 +37,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 17 ^Q :ed_ignore, # 18 ^R - :ed_search_prev_history, + :vi_search_prev, # 19 ^S :ed_ignore, # 20 ^T @@ -151,7 +151,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 74 J :vi_join_lines, # 75 K - :ed_search_prev_history, + :vi_search_prev, # 76 L :ed_unassigned, # 77 M diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index 06e94a9c30..b8e89f81d8 100644 --- a/lib/reline/key_actor/vi_insert.rb +++ b/lib/reline/key_actor/vi_insert.rb @@ -37,9 +37,9 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base # 17 ^Q :ed_ignore, # 18 ^R - :ed_search_prev_history, + :vi_search_prev, # 19 ^S - :ed_search_next_history, + :vi_search_next, # 20 ^T :ed_insert, # 21 ^U diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index 83136edcf8..017e3db00a 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -42,6 +42,8 @@ class Reline::KeyStroke expand(expand(rhs_bytes) + expand(input.drop(lhs.size))) when Symbol [rhs] + expand(input.drop(lhs.size)) + when Array + rhs end end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 095a7b5a09..9bdccae9c9 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -2,7 +2,6 @@ require 'reline/kill_ring' require 'reline/unicode' require 'tempfile' -require 'pathname' class Reline::LineEditor # TODO: undo @@ -51,12 +50,6 @@ class Reline::LineEditor CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer) MenuInfo = Struct.new('MenuInfo', :target, :list) - CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/ - OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/ - NON_PRINTING_START = "\1" - NON_PRINTING_END = "\2" - WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/ - def initialize(config, encoding) @config = config @completion_append_character = '' @@ -76,11 +69,35 @@ class Reline::LineEditor if @prompt_proc prompt_list = @prompt_proc.(buffer) prompt_list.map!{ prompt } if @vi_arg or @searching_prompt + if @config.show_mode_in_prompt + if @config.editing_mode_is?(:vi_command) + mode_icon = @config.vi_cmd_mode_icon + elsif @config.editing_mode_is?(:vi_insert) + mode_icon = @config.vi_ins_mode_icon + elsif @config.editing_mode_is?(:emacs) + mode_icon = @config.emacs_mode_string + else + mode_icon = '?' + end + prompt_list.map!{ |pr| mode_icon + pr } + end prompt = prompt_list[@line_index] prompt_width = calculate_width(prompt, true) [prompt, prompt_width, prompt_list] else prompt_width = calculate_width(prompt, true) + if @config.show_mode_in_prompt + if @config.editing_mode_is?(:vi_command) + mode_icon = @config.vi_cmd_mode_icon + elsif @config.editing_mode_is?(:vi_insert) + mode_icon = @config.vi_ins_mode_icon + elsif @config.editing_mode_is?(:emacs) + mode_icon = @config.emacs_mode_string + else + mode_icon = '?' + end + prompt = mode_icon + prompt + end [prompt, prompt_width, nil] end end @@ -116,7 +133,7 @@ class Reline::LineEditor if @line_index.zero? 0 else - calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list) + calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end if @prompt_proc prompt = prompt_list[@line_index] @@ -190,10 +207,10 @@ class Reline::LineEditor @is_multiline = false end - private def calculate_height_by_lines(lines, prompt_list) + private def calculate_height_by_lines(lines, prompt) result = 0 + prompt_list = prompt.is_a?(Array) ? prompt : nil lines.each_with_index { |line, i| - prompt = '' prompt = prompt_list[i] if prompt_list and prompt_list[i] result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line)) } @@ -211,40 +228,8 @@ class Reline::LineEditor width.div(@screen_size.last) + 1 end - private def split_by_width(prompt, str, max_width) - lines = [String.new(encoding: @encoding)] - height = 1 - width = 0 - rest = "#{prompt}#{str}".encode(Encoding::UTF_8) - in_zero_width = false - rest.scan(WIDTH_SCANNER) do |gc| - case gc - when NON_PRINTING_START - in_zero_width = true - when NON_PRINTING_END - in_zero_width = false - when CSI_REGEXP, OSC_REGEXP - lines.last << gc - else - unless in_zero_width - mbchar_width = Reline::Unicode.get_mbchar_width(gc) - if (width += mbchar_width) > max_width - width = mbchar_width - lines << nil - lines << String.new(encoding: @encoding) - height += 1 - end - end - lines.last << gc - end - end - # The cursor moves to next line in first - if width == max_width - lines << nil - lines << String.new(encoding: @encoding) - height += 1 - end - [lines, height] + private def split_by_width(str, max_width) + Reline::Unicode.split_by_width(str, max_width, @encoding) end private def scroll_down(val) @@ -358,7 +343,7 @@ class Reline::LineEditor new_lines = whole_lines end prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt) - all_height = calculate_height_by_lines(new_lines, prompt_list) + all_height = calculate_height_by_lines(new_lines, prompt_list || prompt) diff = all_height - @highest_in_all move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1) if diff > 0 @@ -398,7 +383,7 @@ class Reline::LineEditor if @line_index.zero? 0 else - calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list) + calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end if @prompt_proc prompt = prompt_list[@line_index] @@ -457,7 +442,7 @@ class Reline::LineEditor if @line_index.zero? 0 else - calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list) + calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) end @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 move_cursor_down(@first_line_started_from + @started_from) @@ -488,7 +473,7 @@ class Reline::LineEditor end private def render_partial(prompt, prompt_width, line_to_render, with_control = true) - visual_lines, height = split_by_width(prompt, line_to_render.nil? ? '' : line_to_render, @screen_size.last) + visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last) if with_control if height > @highest_in_this diff = height - @highest_in_this @@ -507,8 +492,18 @@ class Reline::LineEditor Reline::IOGate.move_cursor_column(0) visual_lines.each_with_index do |line, index| if line.nil? - if Reline::IOGate.win? and calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last - # A newline is automatically inserted if a character is rendered at eol on command prompt. + if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last + # reaches the end of line + if Reline::IOGate.win? + # A newline is automatically inserted if a character is rendered at + # eol on command prompt. + else + # When the cursor is at the end of the line and erases characters + # after the cursor, some terminals delete the character at the + # cursor position. + move_cursor_down(1) + Reline::IOGate.move_cursor_column(0) + end else Reline::IOGate.erase_after_cursor move_cursor_down(1) @@ -529,12 +524,14 @@ class Reline::LineEditor end Reline::IOGate.erase_after_cursor if with_control - move_cursor_up(height - 1) + # Just after rendring, so the cursor is on the last line. if finished? - move_cursor_down(@started_from) + Reline::IOGate.move_cursor_column(0) + else + # Moves up from bottom of lines to the cursor position. + move_cursor_up(height - 1 - @started_from) + Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end - move_cursor_down(@started_from) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end height end @@ -543,7 +540,7 @@ class Reline::LineEditor return before if before.nil? || before.empty? if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?) - after.lines("\n", chomp: true) + after.lines("\n").map { |l| l.chomp('') } else before end @@ -1058,29 +1055,7 @@ class Reline::LineEditor end private def calculate_width(str, allow_escape_code = false) - if allow_escape_code - width = 0 - rest = str.encode(Encoding::UTF_8) - in_zero_width = false - rest.scan(WIDTH_SCANNER) do |gc| - case gc - when NON_PRINTING_START - in_zero_width = true - when NON_PRINTING_END - in_zero_width = false - when CSI_REGEXP, OSC_REGEXP - else - unless in_zero_width - width += Reline::Unicode.get_mbchar_width(gc) - end - end - end - width - else - str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |w, gc| - w + Reline::Unicode.get_mbchar_width(gc) - } - end + Reline::Unicode.calculate_width(str, allow_escape_code) end private def key_delete(key) @@ -1258,7 +1233,7 @@ class Reline::LineEditor if search_word.empty? and Reline.last_incremental_search search_word = Reline.last_incremental_search end - if @history_pointer # TODO + if @history_pointer case prev_search_key when "\C-r".ord history_pointer_base = 0 @@ -1330,7 +1305,7 @@ class Reline::LineEditor end end - private def search_history(key) + private def incremental_search_history(key) unless @history_pointer if @is_multiline @line_backup_in_history = whole_buffer @@ -1411,15 +1386,114 @@ class Reline::LineEditor } end - private def ed_search_prev_history(key) - search_history(key) + private def vi_search_prev(key) + incremental_search_history(key) end - alias_method :reverse_search_history, :ed_search_prev_history + alias_method :reverse_search_history, :vi_search_prev - private def ed_search_next_history(key) - search_history(key) + private def vi_search_next(key) + incremental_search_history(key) end - alias_method :forward_search_history, :ed_search_next_history + alias_method :forward_search_history, :vi_search_next + + private def ed_search_prev_history(key, arg: 1) + history = nil + h_pointer = nil + line_no = nil + substr = @line.slice(0, @byte_pointer) + if @history_pointer.nil? + return if not @line.empty? and substr.empty? + history = Reline::HISTORY + elsif @history_pointer.zero? + history = nil + h_pointer = nil + else + history = Reline::HISTORY.slice(0, @history_pointer) + end + return if history.nil? + if @is_multiline + h_pointer = history.rindex { |h| + h.split("\n").each_with_index { |l, i| + if l.start_with?(substr) + line_no = i + break + end + } + not line_no.nil? + } + else + h_pointer = history.rindex { |l| + l.start_with?(substr) + } + end + return if h_pointer.nil? + @history_pointer = h_pointer + if @is_multiline + @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = line_no + @line = @buffer_of_lines.last + @rerender_all = true + else + @line = Reline::HISTORY[@history_pointer] + end + @cursor_max = calculate_width(@line) + arg -= 1 + ed_search_prev_history(key, arg: arg) if arg > 0 + end + alias_method :history_search_backward, :ed_search_prev_history + + private def ed_search_next_history(key, arg: 1) + substr = @line.slice(0, @byte_pointer) + if @history_pointer.nil? + return + elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty? + return + end + history = Reline::HISTORY.slice((@history_pointer + 1)..-1) + h_pointer = nil + line_no = nil + if @is_multiline + h_pointer = history.index { |h| + h.split("\n").each_with_index { |l, i| + if l.start_with?(substr) + line_no = i + break + end + } + not line_no.nil? + } + else + h_pointer = history.index { |l| + l.start_with?(substr) + } + end + h_pointer += @history_pointer + 1 if h_pointer and @history_pointer + return if h_pointer.nil? and not substr.empty? + @history_pointer = h_pointer + if @is_multiline + if @history_pointer.nil? and substr.empty? + @buffer_of_lines = [] + @line_index = 0 + else + @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") + @line_index = line_no + end + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line = @buffer_of_lines.last + @rerender_all = true + else + if @history_pointer.nil? and substr.empty? + @line = '' + else + @line = Reline::HISTORY[@history_pointer] + end + end + @cursor_max = calculate_width(@line) + arg -= 1 + ed_search_next_history(key, arg: arg) if arg > 0 + end + alias_method :history_search_forward, :ed_search_next_history private def ed_prev_history(key, arg: 1) if @is_multiline and @line_index > 0 @@ -2022,7 +2096,7 @@ class Reline::LineEditor fp.path } system("#{ENV['EDITOR']} #{path}") - @line = Pathname.new(path).read + @line = File.read(path) finish end @@ -2222,7 +2296,6 @@ class Reline::LineEditor new_pointer = [@byte_pointer, @line_index] @previous_line_index = @line_index @byte_pointer, @line_index = @mark_pointer - @byte_pointer, @line_index = @mark_pointer @cursor = calculate_width(@line.byteslice(0, @byte_pointer)) @cursor_max = calculate_width(@line) @mark_pointer = new_pointer diff --git a/lib/reline/reline.gemspec b/lib/reline/reline.gemspec index 3ced4b8488..1962f61def 100644 --- a/lib/reline/reline.gemspec +++ b/lib/reline/reline.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |spec| spec.summary = %q{Alternative GNU Readline or Editline implementation by pure Ruby.} spec.description = %q{Alternative GNU Readline or Editline implementation by pure Ruby.} spec.homepage = 'https://github.com/ruby/reline' - spec.license = 'Ruby License' + spec.license = 'Ruby' spec.files = Dir['BSDL', 'COPYING', 'README.md', 'lib/**/*'] spec.require_paths = ['lib'] diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 4b30f044f3..cd8c27e85b 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -35,6 +35,12 @@ class Reline::Unicode } EscapedChars = EscapedPairs.keys.map(&:chr) + CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/ + OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/ + NON_PRINTING_START = "\1" + NON_PRINTING_END = "\2" + WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/ + def self.get_mbchar_byte_size_by_first_char(c) # Checks UTF-8 character byte size case c.ord @@ -85,6 +91,68 @@ class Reline::Unicode end end + def self.calculate_width(str, allow_escape_code = false) + if allow_escape_code + width = 0 + rest = str.encode(Encoding::UTF_8) + in_zero_width = false + rest.scan(WIDTH_SCANNER) do |gc| + case gc + when NON_PRINTING_START + in_zero_width = true + when NON_PRINTING_END + in_zero_width = false + when CSI_REGEXP, OSC_REGEXP + else + unless in_zero_width + width += get_mbchar_width(gc) + end + end + end + width + else + str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |w, gc| + w + get_mbchar_width(gc) + } + end + end + + def self.split_by_width(str, max_width, encoding = str.encoding) + lines = [String.new(encoding: encoding)] + height = 1 + width = 0 + rest = str.encode(Encoding::UTF_8) + in_zero_width = false + rest.scan(WIDTH_SCANNER) do |gc| + case gc + when NON_PRINTING_START + in_zero_width = true + when NON_PRINTING_END + in_zero_width = false + when CSI_REGEXP, OSC_REGEXP + lines.last << gc + else + unless in_zero_width + mbchar_width = get_mbchar_width(gc) + if (width += mbchar_width) > max_width + width = mbchar_width + lines << nil + lines << String.new(encoding: encoding) + height += 1 + end + end + lines.last << gc + end + end + # The cursor moves to next line in first + if width == max_width + lines << nil + lines << String.new(encoding: encoding) + height += 1 + end + [lines, height] + end + def self.get_next_mbchar_size(line, byte_pointer) grapheme = line.byteslice(byte_pointer..-1).grapheme_clusters.first grapheme ? grapheme.bytesize : 0 diff --git a/lib/reline/version.rb b/lib/reline/version.rb index 1bf544d74b..aa0ef18145 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.1.3' + VERSION = '0.1.5' end diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb index c229c8536f..2a406e39d3 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/windows.rb @@ -92,6 +92,7 @@ class Reline::Windows @@ReadConsoleInput = Win32API.new('kernel32', 'ReadConsoleInput', ['L', 'P', 'L', 'P'], 'L') @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L') @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I') + @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L') @@input_buf = [] @@output_buf = [] @@ -249,9 +250,17 @@ class Reline::Windows end def self.clear_screen - # TODO: Use FillConsoleOutputCharacter and FillConsoleOutputAttribute - write "\e[2J" - write "\e[1;1H" + csbi = 0.chr * 22 + return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0 + buffer_width = csbi[0, 2].unpack('S').first + attributes = csbi[8, 2].unpack('S').first + _window_left, window_top, _window_right, window_bottom = *csbi[10,8].unpack('S*') + fill_length = buffer_width * (window_bottom - window_top + 1) + screen_topleft = window_top * 65536 + written = 0.chr * 4 + @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written) + @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written) + @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft) end def self.set_screen_size(rows, columns) diff --git a/test/reline/test_config.rb b/test/reline/test_config.rb index cecb364f89..c1455cd136 100644 --- a/test/reline/test_config.rb +++ b/test/reline/test_config.rb @@ -36,6 +36,51 @@ class Reline::Config::Test < Reline::TestCase assert_equal true, @config.instance_variable_get(:@disable_completion) end + def test_string_value + @config.read_lines(<<~LINES.lines) + set show-mode-in-prompt on + set emacs-mode-string Emacs + LINES + + assert_equal 'Emacs', @config.instance_variable_get(:@emacs_mode_string) + end + + def test_string_value_with_brackets + @config.read_lines(<<~LINES.lines) + set show-mode-in-prompt on + set emacs-mode-string [Emacs] + LINES + + assert_equal '[Emacs]', @config.instance_variable_get(:@emacs_mode_string) + end + + def test_string_value_with_brackets_and_quotes + @config.read_lines(<<~LINES.lines) + set show-mode-in-prompt on + set emacs-mode-string "[Emacs]" + LINES + + assert_equal '[Emacs]', @config.instance_variable_get(:@emacs_mode_string) + end + + def test_string_value_with_parens + @config.read_lines(<<~LINES.lines) + set show-mode-in-prompt on + set emacs-mode-string (Emacs) + LINES + + assert_equal '(Emacs)', @config.instance_variable_get(:@emacs_mode_string) + end + + def test_string_value_with_parens_and_quotes + @config.read_lines(<<~LINES.lines) + set show-mode-in-prompt on + set emacs-mode-string "(Emacs)" + LINES + + assert_equal '(Emacs)', @config.instance_variable_get(:@emacs_mode_string) + end + def test_comment_line @config.read_lines([" #a: error\n"]) assert_not_include @config.key_bindings, nil @@ -213,6 +258,68 @@ class Reline::Config::Test < Reline::TestCase assert_nothing_raised do @config.read end + ensure + ENV['INPUTRC'] = inputrc_backup + end + + def test_inputrc + inputrc_backup = ENV['INPUTRC'] + expected = "#{@tmpdir}/abcde" + ENV['INPUTRC'] = expected + assert_equal expected, @config.inputrc_path + ensure ENV['INPUTRC'] = inputrc_backup end + + def test_xdg_config_home + home_backup = ENV['HOME'] + xdg_config_home_backup = ENV['XDG_CONFIG_HOME'] + xdg_config_home = File.expand_path("#{@tmpdir}/.config/example_dir") + expected = File.expand_path("#{xdg_config_home}/readline/inputrc") + FileUtils.mkdir_p(File.dirname(expected)) + FileUtils.touch(expected) + ENV['HOME'] = @tmpdir + ENV['XDG_CONFIG_HOME'] = xdg_config_home + assert_equal expected, @config.inputrc_path + ensure + FileUtils.rm(expected) + ENV['XDG_CONFIG_HOME'] = xdg_config_home_backup + ENV['HOME'] = home_backup + end + + def test_empty_xdg_config_home + home_backup = ENV['HOME'] + xdg_config_home_backup = ENV['XDG_CONFIG_HOME'] + ENV['HOME'] = @tmpdir + ENV['XDG_CONFIG_HOME'] = '' + expected = File.expand_path('~/.config/readline/inputrc') + FileUtils.mkdir_p(File.dirname(expected)) + FileUtils.touch(expected) + assert_equal expected, @config.inputrc_path + ensure + FileUtils.rm(expected) + ENV['XDG_CONFIG_HOME'] = xdg_config_home_backup + ENV['HOME'] = home_backup + end + + def test_relative_xdg_config_home + home_backup = ENV['HOME'] + xdg_config_home_backup = ENV['XDG_CONFIG_HOME'] + ENV['HOME'] = @tmpdir + expected = File.expand_path('~/.config/readline/inputrc') + FileUtils.mkdir_p(File.dirname(expected)) + FileUtils.touch(expected) + result = Dir.chdir(@tmpdir) do + xdg_config_home = ".config/example_dir" + ENV['XDG_CONFIG_HOME'] = xdg_config_home + inputrc = "#{xdg_config_home}/readline/inputrc" + FileUtils.mkdir_p(File.dirname(inputrc)) + FileUtils.touch(inputrc) + @config.inputrc_path + end + assert_equal expected, result + FileUtils.rm(expected) + ENV['XDG_CONFIG_HOME'] = xdg_config_home_backup + ENV['HOME'] = home_backup + end end diff --git a/test/reline/test_history.rb b/test/reline/test_history.rb index 189f2db86d..58c240fc96 100644 --- a/test/reline/test_history.rb +++ b/test/reline/test_history.rb @@ -242,6 +242,26 @@ class Reline::History::Test < Reline::TestCase end end + def test_history_size_zero + history = history_new(history_size: 0) + assert_equal 0, history.size + history << 'aa' + history << 'bb' + assert_equal 0, history.size + history.push(*%w{aa bb cc}) + assert_equal 0, history.size + end + + def test_history_size_negative_unlimited + history = history_new(history_size: -1) + assert_equal 0, history.size + history << 'aa' + history << 'bb' + assert_equal 2, history.size + history.push(*%w{aa bb cc}) + assert_equal 5, history.size + end + private def history_new(history_size: 10) diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index c16212c626..a1e4015999 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -1625,7 +1625,7 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase assert_line('') end - def test_ed_search_prev_history + def test_vi_search_prev Reline::HISTORY.concat(%w{abc 123 AAA}) assert_line('') assert_byte_pointer_size('') @@ -1897,12 +1897,162 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase end def test_modify_lines_with_wrong_rs + verbose, $VERBOSE = $VERBOSE, nil original_global_slash = $/ $/ = 'b' + $VERBOSE = verbose @line_editor.output_modifier_proc = proc { |output| Reline::Unicode.escape_for_print(output) } input_keys("abcdef\n") - assert_equal(['abcdef'], @line_editor.__send__(:modify_lines, @line_editor.whole_lines)) + result = @line_editor.__send__(:modify_lines, @line_editor.whole_lines) + $/ = nil + assert_equal(['abcdef'], result) + ensure + $VERBOSE = nil $/ = original_global_slash + $VERBOSE = verbose + end + + def test_ed_search_prev_history + Reline::HISTORY.concat([ + '12356', # old + '12aaa', + '12345' # new + ]) + input_keys('123') + # The ed_search_prev_history doesn't have default binding + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12356') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12356') + end + + def test_ed_search_prev_history_with_empty + Reline::HISTORY.concat([ + '12356', # old + '12aaa', + '12345' # new + ]) + # The ed_search_prev_history doesn't have default binding + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12aaa') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12356') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12356') + end + + def test_ed_search_prev_history_without_match + Reline::HISTORY.concat([ + '12356', # old + '12aaa', + '12345' # new + ]) + input_keys('ABC') + # The ed_search_prev_history doesn't have default binding + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('ABC') + assert_cursor(3) + assert_cursor_max(3) + assert_line('ABC') + end + + def test_ed_search_next_history + Reline::HISTORY.concat([ + '12356', # old + '12aaa', + '12345' # new + ]) + input_keys('123') + # The ed_search_prev_history and ed_search_next_history doesn't have default binding + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12356') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12356') + @line_editor.__send__(:ed_search_next_history, "\C-n".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_next_history, "\C-n".ord) + assert_byte_pointer_size('123') + assert_cursor(3) + assert_cursor_max(5) + assert_line('12345') + end + + def test_ed_search_next_history_with_empty + Reline::HISTORY.concat([ + '12356', # old + '12aaa', + '12345' # new + ]) + # The ed_search_prev_history and ed_search_next_history doesn't have default binding + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12aaa') + @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12356') + @line_editor.__send__(:ed_search_next_history, "\C-n".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12aaa') + @line_editor.__send__(:ed_search_next_history, "\C-n".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(5) + assert_line('12345') + @line_editor.__send__(:ed_search_next_history, "\C-n".ord) + assert_byte_pointer_size('') + assert_cursor(0) + assert_cursor_max(0) + assert_line('') end =begin # TODO: move KeyStroke instance from Reline to LineEditor diff --git a/test/reline/test_key_stroke.rb b/test/reline/test_key_stroke.rb index 224e93d9a0..15675a9b5a 100644 --- a/test/reline/test_key_stroke.rb +++ b/test/reline/test_key_stroke.rb @@ -16,10 +16,10 @@ class Reline::KeyStroke::Test < Reline::TestCase def test_match_status config = Reline::Config.new { - "a" => "xx", - "ab" => "y", - "abc" => "z", - "x" => "rr" + 'a' => 'xx', + 'ab' => 'y', + 'abc' => 'z', + 'x' => 'rr' }.each_pair do |key, func| config.add_default_key_binding(key.bytes, func.bytes) end @@ -35,4 +35,15 @@ class Reline::KeyStroke::Test < Reline::TestCase assert_equal(:unmatched, stroke.match_status("m".bytes)) assert_equal(:matched, stroke.match_status("abzwabk".bytes)) end + + def test_aaa + config = Reline::Config.new + { + 'abc' => '123', + }.each_pair do |key, func| + config.add_default_key_binding(key.bytes, func.bytes) + end + stroke = Reline::KeyStroke.new(config) + assert_equal('123'.bytes, stroke.expand('abc'.bytes)) + end end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index 0de2462a08..d2de4690d5 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -7,6 +7,12 @@ class Reline::Test < Reline::TestCase end def setup + Reline.output_modifier_proc = nil + Reline.completion_proc = nil + Reline.prompt_proc = nil + Reline.auto_indent_proc = nil + Reline.pre_input_hook = nil + Reline.dig_perfect_match_proc = nil end def teardown @@ -14,6 +20,8 @@ class Reline::Test < Reline::TestCase end def test_completion_append_character + completion_append_character = Reline.completion_append_character + assert_equal(nil, Reline.completion_append_character) Reline.completion_append_character = "" @@ -33,57 +41,85 @@ class Reline::Test < Reline::TestCase Reline.completion_append_character = nil assert_equal(nil, Reline.completion_append_character) + ensure + Reline.completion_append_character = completion_append_character end def test_basic_word_break_characters + basic_word_break_characters = Reline.basic_word_break_characters + assert_equal(" \t\n`><=;|&{(", Reline.basic_word_break_characters) Reline.basic_word_break_characters = "[".encode(Encoding::ASCII) assert_equal("[", Reline.basic_word_break_characters) assert_equal(get_reline_encoding, Reline.basic_word_break_characters.encoding) + ensure + Reline.basic_word_break_characters = basic_word_break_characters end def test_completer_word_break_characters + completer_word_break_characters = Reline.completer_word_break_characters + assert_equal(" \t\n`><=;|&{(", Reline.completer_word_break_characters) Reline.completer_word_break_characters = "[".encode(Encoding::ASCII) assert_equal("[", Reline.completer_word_break_characters) assert_equal(get_reline_encoding, Reline.completer_word_break_characters.encoding) + ensure + Reline.completer_word_break_characters = completer_word_break_characters end def test_basic_quote_characters + basic_quote_characters = Reline.basic_quote_characters + assert_equal('"\'', Reline.basic_quote_characters) Reline.basic_quote_characters = "`".encode(Encoding::ASCII) assert_equal("`", Reline.basic_quote_characters) assert_equal(get_reline_encoding, Reline.basic_quote_characters.encoding) + ensure + Reline.basic_quote_characters = basic_quote_characters end def test_completer_quote_characters + completer_quote_characters = Reline.completer_quote_characters + assert_equal('"\'', Reline.completer_quote_characters) Reline.completer_quote_characters = "`".encode(Encoding::ASCII) assert_equal("`", Reline.completer_quote_characters) assert_equal(get_reline_encoding, Reline.completer_quote_characters.encoding) + ensure + Reline.completer_quote_characters = completer_quote_characters end def test_filename_quote_characters + filename_quote_characters = Reline.filename_quote_characters + assert_equal('', Reline.filename_quote_characters) Reline.filename_quote_characters = "\'".encode(Encoding::ASCII) assert_equal("\'", Reline.filename_quote_characters) assert_equal(get_reline_encoding, Reline.filename_quote_characters.encoding) + ensure + Reline.filename_quote_characters = filename_quote_characters end def test_special_prefixes + special_prefixes = Reline.special_prefixes + assert_equal('', Reline.special_prefixes) Reline.special_prefixes = "\'".encode(Encoding::ASCII) assert_equal("\'", Reline.special_prefixes) assert_equal(get_reline_encoding, Reline.special_prefixes.encoding) + ensure + Reline.special_prefixes = special_prefixes end def test_completion_case_fold + completion_case_fold = Reline.completion_case_fold + assert_equal(nil, Reline.completion_case_fold) Reline.completion_case_fold = true @@ -91,6 +127,8 @@ class Reline::Test < Reline::TestCase Reline.completion_case_fold = "hoge".encode(Encoding::ASCII) assert_equal("hoge", Reline.completion_case_fold) + ensure + Reline.completion_case_fold = completion_case_fold end def test_completion_proc diff --git a/test/reline/test_within_pipe.rb b/test/reline/test_within_pipe.rb index 46b4465f32..53989a794f 100644 --- a/test/reline/test_within_pipe.rb +++ b/test/reline/test_within_pipe.rb @@ -3,9 +3,10 @@ require_relative 'helper' class Reline::WithinPipeTest < Reline::TestCase def setup Reline.send(:test_mode) - @reader, @writer = IO.pipe((RELINE_TEST_ENCODING rescue Encoding.default_external)) - Reline.input = @reader - @output = Reline.output = File.open(IO::NULL, 'w') + @input_reader, @writer = IO.pipe((RELINE_TEST_ENCODING rescue Encoding.default_external)) + Reline.input = @input_reader + @reader, @output_writer = IO.pipe((RELINE_TEST_ENCODING rescue Encoding.default_external)) + @output = Reline.output = @output_writer @config = Reline.send(:core).config @line_editor = Reline.send(:core).line_editor end @@ -14,9 +15,11 @@ class Reline::WithinPipeTest < Reline::TestCase Reline.input = STDIN Reline.output = STDOUT Reline.point = 0 - @reader.close + Reline.delete_text + @input_reader.close @writer.close - @output.close + @reader.close + @output_writer.close @config.reset end diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 4eab6661d6..0ab43fa60c 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -5,14 +5,29 @@ begin class Reline::TestRendering < Yamatanooroti::TestCase def setup - inputrc_backup = ENV['INPUTRC'] - ENV['INPUTRC'] = 'nonexistent_file' - start_terminal(5, 30, %w{ruby -Ilib bin/multiline_repl}) - sleep 0.5 - ENV['INPUTRC'] = inputrc_backup + @pwd = Dir.pwd + @tmpdir = File.join(File.expand_path(Dir.tmpdir), "test_reline_config_#{$$}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST + FileUtils.rm_rf(@tmpdir) + Dir.mkdir(@tmpdir) + end + Dir.chdir(@tmpdir) + @inputrc_backup = ENV['INPUTRC'] + @inputrc_file = ENV['INPUTRC'] = File.join(@tmpdir, 'temporaty_inputrc') + File.unlink(@inputrc_file) if File.exist?(@inputrc_file) + end + + def teardown + Dir.chdir(@pwd) + FileUtils.rm_rf(@tmpdir) + ENV['INPUTRC'] = @inputrc_backup end def test_history_back + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 write(":a\n") write("\C-p") close @@ -25,6 +40,8 @@ begin end def test_backspace + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 write(":abc\C-h\n") close assert_screen(<<~EOC) @@ -34,6 +51,172 @@ begin prompt> EOC end + + def test_autowrap + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write('01234567890123456789012') + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> 0123456789012345678901 + 2 + EOC + end + + def test_finish_autowrapped_line + start_terminal(10, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write("[{'user'=>{'email'=>'a@a', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]\n") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> [{'user'=>{'email'=>'a@a', 'id'= + >'ABC'}, 'version'=>4, 'status'=>'succee + ded'}] + => [{"user"=>{"email"=>"a@a", "id"=>"ABC + "}, "version"=>4, "status"=>"succeeded"} + ] + prompt> + EOC + end + + def test_finish_autowrapped_line_in_the_middle_of_lines + start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write("[{'user'=>{'email'=>'abcdef@abcdef', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]#{"\C-b"*7}\n") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> [{'user'=>{'email'=>'a + bcdef@abcdef', 'id'=>'ABC'}, ' + version'=>4, 'status'=>'succee + ded'}] + => [{"user"=>{"email"=>"abcdef + @abcdef", "id"=>"ABC"}, "versi + on"=>4, "status"=>"succeeded"} + ] + prompt> + EOC + end + + def test_finish_autowrapped_line_in_the_middle_of_multilines + start_terminal(30, 16, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write("<<~EOM\n ABCDEFG\nEOM\n") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> <<~EOM + prompt> ABCDEF + G + prompt> EOM + => "ABCDEFG\n" + prompt> + EOC + end + + def test_prompt + File.open(@inputrc_file, 'w') do |f| + f.write <<~'LINES' + "abc": "123" + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write("abc\n") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> 123 + => 123 + prompt> + EOC + end + + def test_mode_icon_emacs + File.open(@inputrc_file, 'w') do |f| + f.write <<~LINES + set show-mode-in-prompt on + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + close + assert_screen(<<~EOC) + Multiline REPL. + @prompt> + EOC + end + + def test_mode_icon_vi + File.open(@inputrc_file, 'w') do |f| + f.write <<~LINES + set editing-mode vi + set show-mode-in-prompt on + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + sleep 0.5 + write(":a\n\C-[k") + close + assert_screen(<<~EOC) + Multiline REPL. + (ins)prompt> :a + => :a + (cmd)prompt> :a + EOC + end + + def test_original_mode_icon_emacs + File.open(@inputrc_file, 'w') do |f| + f.write <<~LINES + set show-mode-in-prompt on + set emacs-mode-string [emacs] + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + close + assert_screen(<<~EOC) + Multiline REPL. + [emacs]prompt> + EOC + end + + def test_original_mode_icon_with_quote + File.open(@inputrc_file, 'w') do |f| + f.write <<~LINES + set show-mode-in-prompt on + set emacs-mode-string "[emacs]" + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + close + assert_screen(<<~EOC) + Multiline REPL. + [emacs]prompt> + EOC + end + + def test_original_mode_icon_vi + File.open(@inputrc_file, 'w') do |f| + f.write <<~LINES + set editing-mode vi + set show-mode-in-prompt on + set vi-ins-mode-string "{InS}" + set vi-cmd-mode-string "{CmD}" + LINES + end + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}) + write(":a\n\C-[k") + close + assert_screen(<<~EOC) + Multiline REPL. + {InS}prompt> :a + => :a + {CmD}prompt> :a + EOC + end end rescue LoadError, NameError # On Ruby repository, this test suit doesn't run because Ruby repo doesn't |