diff options
author | aycabta <aycabta@gmail.com> | 2020-09-16 11:19:06 +0900 |
---|---|---|
committer | nagachika <nagachika@ruby-lang.org> | 2020-09-16 21:07:25 +0900 |
commit | 3bb503e0e8f92c039ce50f430b14649a36c03feb (patch) | |
tree | cf88e25a2d373ed72fa46c4998de5fa12abd1f97 /lib | |
parent | ed39078d37e16b541d717cd87cacb21aa33e6ef1 (diff) |
Merge Reline 0.1.5
Diffstat (limited to 'lib')
-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 |
13 files changed, 386 insertions, 137 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) |