require 'reline/kill_ring' require 'reline/unicode' require 'tempfile' class Reline::LineEditor # TODO: undo attr_reader :line attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc attr_accessor :completion_proc attr_accessor :completion_append_character attr_accessor :output_modifier_proc attr_accessor :prompt_proc attr_accessor :auto_indent_proc attr_accessor :pre_input_hook attr_accessor :dig_perfect_match_proc attr_writer :output VI_MOTIONS = %i{ ed_prev_char ed_next_char vi_zero ed_move_to_beg ed_move_to_end vi_to_column vi_next_char vi_prev_char vi_next_word vi_prev_word vi_to_next_char vi_to_prev_char vi_end_word vi_next_big_word vi_prev_big_word vi_end_big_word vi_repeat_next_char vi_repeat_prev_char } module CompletionState NORMAL = :normal COMPLETION = :completion MENU = :menu JOURNEY = :journey MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match PERFECT_MATCH = :perfect_match end CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer) MenuInfo = Struct.new('MenuInfo', :target, :list) PROMPT_LIST_CACHE_TIMEOUT = 0.5 def initialize(config, encoding) @config = config @completion_append_character = '' reset_variables(encoding: encoding) end def set_pasting_state(in_pasting) @in_pasting = in_pasting end def simplified_rendering? if finished? false elsif @just_cursor_moving and not @rerender_all true else not @rerender_all and not finished? and @in_pasting end end private def check_mode_string mode_string = nil if @config.show_mode_in_prompt if @config.editing_mode_is?(:vi_command) mode_string = @config.vi_cmd_mode_string elsif @config.editing_mode_is?(:vi_insert) mode_string = @config.vi_ins_mode_string elsif @config.editing_mode_is?(:emacs) mode_string = @config.emacs_mode_string else mode_string = '?' end end if mode_string != @prev_mode_string @rerender_all = true end @prev_mode_string = mode_string mode_string end private def check_multiline_prompt(buffer, prompt) if @vi_arg prompt = "(arg: #{@vi_arg}) " @rerender_all = true elsif @searching_prompt prompt = @searching_prompt @rerender_all = true else prompt = @prompt end if simplified_rendering? mode_string = check_mode_string prompt = mode_string + prompt if mode_string return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] end if @prompt_proc use_cached_prompt_list = false if @cached_prompt_list if @just_cursor_moving use_cached_prompt_list = true elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size use_cached_prompt_list = true end end use_cached_prompt_list = false if @rerender_all if use_cached_prompt_list prompt_list = @cached_prompt_list else prompt_list = @cached_prompt_list = @prompt_proc.(buffer) @prompt_cache_time = Time.now.to_f end prompt_list.map!{ prompt } if @vi_arg or @searching_prompt prompt_list = [prompt] if prompt_list.empty? mode_string = check_mode_string prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string prompt = prompt_list[@line_index] prompt = prompt_list[0] if prompt.nil? prompt = prompt_list.last if prompt.nil? if buffer.size > prompt_list.size (buffer.size - prompt_list.size).times do prompt_list << prompt_list.last end end prompt_width = calculate_width(prompt, true) [prompt, prompt_width, prompt_list] else mode_string = check_mode_string prompt = mode_string + prompt if mode_string prompt_width = calculate_width(prompt, true) [prompt, prompt_width, nil] end end def reset(prompt = '', encoding:) @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y @screen_size = Reline::IOGate.get_screen_size @screen_height = @screen_size.first reset_variables(prompt, encoding: encoding) @old_trap = Signal.trap('SIGINT') { if @scroll_partial_screen move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1) else move_cursor_down(@highest_in_all - @line_index - 1) end Reline::IOGate.move_cursor_column(0) scroll_down(1) @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT" raise Interrupt } Reline::IOGate.set_winch_handler do @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y old_screen_size = @screen_size @screen_size = Reline::IOGate.get_screen_size @screen_height = @screen_size.first if old_screen_size.last < @screen_size.last # columns increase @rerender_all = true rerender else back = 0 new_buffer = whole_lines prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt) new_buffer.each_with_index do |line, index| prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc width = prompt_width + calculate_width(line) height = calculate_height_by_width(width) back += height end @highest_in_all = back @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) @first_line_started_from = if @line_index.zero? 0 else calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end if @prompt_proc prompt = prompt_list[@line_index] prompt_width = calculate_width(prompt, true) end calculate_nearest_cursor @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) @rerender_all = true end end end def finalize Signal.trap('SIGINT', @old_trap) end def eof? @eof end def reset_variables(prompt = '', encoding:) @prompt = prompt @mark_pointer = nil @encoding = encoding @is_multiline = false @finished = false @cleared = false @rerender_all = false @history_pointer = nil @kill_ring ||= Reline::KillRing.new @vi_clipboard = '' @vi_arg = nil @waiting_proc = nil @waiting_operator_proc = nil @waiting_operator_vi_arg = nil @completion_journey_data = nil @completion_state = CompletionState::NORMAL @perfect_matched = nil @menu_info = nil @first_prompt = true @searching_prompt = nil @first_char = true @add_newline_to_end_of_buffer = false @just_cursor_moving = nil @cached_prompt_list = nil @prompt_cache_time = nil @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) @scroll_partial_screen = nil @prev_mode_string = nil @drop_terminate_spaces = false @in_pasting = false @auto_indent_proc = nil reset_line end def reset_line @cursor = 0 @cursor_max = 0 @byte_pointer = 0 @buffer_of_lines = [String.new(encoding: @encoding)] @line_index = 0 @previous_line_index = nil @line = @buffer_of_lines[0] @first_line_started_from = 0 @move_up = 0 @started_from = 0 @highest_in_this = 1 @highest_in_all = 1 @line_backup_in_history = nil @multibyte_buffer = String.new(encoding: 'ASCII-8BIT') @check_new_auto_indent = false end def multiline_on @is_multiline = true end def multiline_off @is_multiline = false end 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_list[i] if prompt_list and prompt_list[i] result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line)) } result end private def insert_new_line(cursor_line, next_line) @line = cursor_line @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding)) @previous_line_index = @line_index @line_index += 1 @just_cursor_moving = false end private def calculate_height_by_width(width) width.div(@screen_size.last) + 1 end private def split_by_width(str, max_width) Reline::Unicode.split_by_width(str, max_width, @encoding) end private def scroll_down(val) if val <= @rest_height Reline::IOGate.move_cursor_down(val) @rest_height -= val else Reline::IOGate.move_cursor_down(@rest_height) Reline::IOGate.scroll_down(val - @rest_height) @rest_height = 0 end end private def move_cursor_up(val) if val > 0 Reline::IOGate.move_cursor_up(val) @rest_height += val elsif val < 0 move_cursor_down(-val) end end private def move_cursor_down(val) if val > 0 Reline::IOGate.move_cursor_down(val) @rest_height -= val @rest_height = 0 if @rest_height < 0 elsif val < 0 move_cursor_up(-val) end end private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true) new_cursor_max = calculate_width(line_to_calc) new_cursor = 0 new_byte_pointer = 0 height = 1 max_width = @screen_size.last if @config.editing_mode_is?(:vi_command) last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize) if last_byte_size > 0 last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size) last_width = Reline::Unicode.get_mbchar_width(last_mbchar) end_of_line_cursor = new_cursor_max - last_width else end_of_line_cursor = new_cursor_max end else end_of_line_cursor = new_cursor_max end line_to_calc.grapheme_clusters.each do |gc| mbchar = gc.encode(Encoding::UTF_8) mbchar_width = Reline::Unicode.get_mbchar_width(mbchar) now = new_cursor + mbchar_width if now > end_of_line_cursor or now > cursor break end new_cursor += mbchar_width if new_cursor > max_width * height height += 1 end new_byte_pointer += gc.bytesize end new_started_from = height - 1 if update @cursor = new_cursor @cursor_max = new_cursor_max @started_from = new_started_from @byte_pointer = new_byte_pointer else [new_cursor, new_cursor_max, new_started_from, new_byte_pointer] end end def rerender_all @rerender_all = true process_insert(force: true) rerender end def rerender return if @line.nil? if @menu_info scroll_down(@highest_in_all - @first_line_started_from) @rerender_all = true end if @menu_info show_menu @menu_info = nil end prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) if @cleared clear_screen_buffer(prompt, prompt_list, prompt_width) @cleared = false return end if @is_multiline and finished? and @scroll_partial_screen # Re-output all code higher than the screen when finished. Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen) Reline::IOGate.move_cursor_column(0) @scroll_partial_screen = nil prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) if @previous_line_index new_lines = whole_lines(index: @previous_line_index, line: @line) else new_lines = whole_lines end modify_lines(new_lines).each_with_index do |line, index| @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n" Reline::IOGate.erase_after_cursor end @output.flush return end new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) # FIXME: end of logical line sometimes breaks rendered = false if @add_newline_to_end_of_buffer rerender_added_newline(prompt, prompt_width) @add_newline_to_end_of_buffer = false else if @just_cursor_moving and not @rerender_all rendered = just_move_cursor @just_cursor_moving = false return elsif @previous_line_index or new_highest_in_this != @highest_in_this rerender_changed_current_line @previous_line_index = nil rendered = true elsif @rerender_all rerender_all_lines @rerender_all = false rendered = true else end end if @is_multiline if finished? # Always rerender on finish because output_modifier_proc may return a different output. if @previous_line_index new_lines = whole_lines(index: @previous_line_index, line: @line) else new_lines = whole_lines end line = modify_lines(new_lines)[@line_index] prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt) render_partial(prompt, prompt_width, line, @first_line_started_from) move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1) scroll_down(1) Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor elsif not rendered unless @in_pasting line = modify_lines(whole_lines)[@line_index] prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) render_partial(prompt, prompt_width, line, @first_line_started_from) end end @buffer_of_lines[@line_index] = @line @rest_height = 0 if @scroll_partial_screen else line = modify_lines(whole_lines)[@line_index] render_partial(prompt, prompt_width, line, 0) if finished? scroll_down(1) Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor end end end private def calculate_scroll_partial_screen(highest_in_all, cursor_y) if @screen_height < highest_in_all old_scroll_partial_screen = @scroll_partial_screen if cursor_y == 0 @scroll_partial_screen = 0 elsif cursor_y == (highest_in_all - 1) @scroll_partial_screen = highest_in_all - @screen_height else if @scroll_partial_screen if cursor_y <= @scroll_partial_screen @scroll_partial_screen = cursor_y elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y @scroll_partial_screen = cursor_y - (@screen_height - 1) end else if cursor_y > (@screen_height - 1) @scroll_partial_screen = cursor_y - (@screen_height - 1) else @scroll_partial_screen = 0 end end end if @scroll_partial_screen != old_scroll_partial_screen @rerender_all = true end else if @scroll_partial_screen @rerender_all = true end @scroll_partial_screen = nil end end private def rerender_added_newline(prompt, prompt_width) scroll_down(1) @buffer_of_lines[@previous_line_index] = @line @line = @buffer_of_lines[@line_index] unless @in_pasting render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false) end @cursor = @cursor_max = calculate_width(@line) @byte_pointer = @line.bytesize @highest_in_all += @highest_in_this @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) @first_line_started_from += @started_from + 1 @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 @previous_line_index = nil end def just_move_cursor prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt) move_cursor_up(@started_from) new_first_line_started_from = if @line_index.zero? 0 else calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end first_line_diff = new_first_line_started_from - @first_line_started_from new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false) new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1 calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from) @previous_line_index = nil if @rerender_all @line = @buffer_of_lines[@line_index] rerender_all_lines @rerender_all = false true else @line = @buffer_of_lines[@line_index] @first_line_started_from = new_first_line_started_from @started_from = new_started_from @cursor = new_cursor @cursor_max = new_cursor_max @byte_pointer = new_byte_pointer move_cursor_down(first_line_diff + @started_from) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) false end end private def rerender_changed_current_line if @previous_line_index new_lines = whole_lines(index: @previous_line_index, line: @line) else 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 || prompt) diff = all_height - @highest_in_all move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1) if diff > 0 scroll_down(diff) move_cursor_up(all_height - 1) elsif diff < 0 (-diff).times do Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor move_cursor_up(1) end move_cursor_up(all_height - 1) else move_cursor_up(all_height - 1) end @highest_in_all = all_height back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width) move_cursor_up(back) if @previous_line_index @buffer_of_lines[@previous_line_index] = @line @line = @buffer_of_lines[@line_index] end @first_line_started_from = if @line_index.zero? 0 else calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end if @prompt_proc prompt = prompt_list[@line_index] prompt_width = calculate_width(prompt, true) end move_cursor_down(@first_line_started_from) calculate_nearest_cursor @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 move_cursor_down(@started_from) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) end private def rerender_all_lines move_cursor_up(@first_line_started_from + @started_from) Reline::IOGate.move_cursor_column(0) back = 0 new_buffer = whole_lines prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt) new_buffer.each_with_index do |line, index| prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc width = prompt_width + calculate_width(line) height = calculate_height_by_width(width) back += height end old_highest_in_all = @highest_in_all if @line_index.zero? new_first_line_started_from = 0 else new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) end new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1 calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from) if @scroll_partial_screen move_cursor_up(@first_line_started_from + @started_from) scroll_down(@screen_height - 1) move_cursor_up(@screen_height) Reline::IOGate.move_cursor_column(0) elsif back > old_highest_in_all scroll_down(back - 1) move_cursor_up(back - 1) elsif back < old_highest_in_all scroll_down(back) Reline::IOGate.erase_after_cursor (old_highest_in_all - back - 1).times do scroll_down(1) Reline::IOGate.erase_after_cursor end move_cursor_up(old_highest_in_all - 1) end render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) if @prompt_proc prompt = prompt_list[@line_index] prompt_width = calculate_width(prompt, true) end @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) @highest_in_all = back @first_line_started_from = new_first_line_started_from @started_from = new_started_from if @scroll_partial_screen Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) else move_cursor_down(@first_line_started_from + @started_from - back + 1) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end end private def render_whole_lines(lines, prompt, prompt_width) rendered_height = 0 modify_lines(lines).each_with_index do |line, index| if prompt.is_a?(Array) line_prompt = prompt[index] prompt_width = calculate_width(line_prompt, true) else line_prompt = prompt end height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false) if index < (lines.size - 1) if @scroll_partial_screen if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height) move_cursor_down(1) end else scroll_down(1) end rendered_height += height else rendered_height += height - 1 end end rendered_height end private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true) visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last) cursor_up_from_last_line = 0 # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index. if @scroll_partial_screen last_visual_line = this_started_from + (height - 1) last_screen_line = @scroll_partial_screen + (@screen_height - 1) if (@scroll_partial_screen - this_started_from) >= height # Render nothing because this line is before the screen. visual_lines = [] elsif this_started_from > last_screen_line # Render nothing because this line is after the screen. visual_lines = [] else deleted_lines_before_screen = [] if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen # A part of visual lines are before the screen. deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2) deleted_lines_before_screen.compact! end if this_started_from <= last_screen_line and last_screen_line < last_visual_line # A part of visual lines are after the screen. visual_lines.pop((last_visual_line - last_screen_line) * 2) end move_cursor_up(deleted_lines_before_screen.size - @started_from) cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size end end if with_control if height > @highest_in_this diff = height - @highest_in_this scroll_down(diff) @highest_in_all += diff @highest_in_this = height move_cursor_up(diff) elsif height < @highest_in_this diff = @highest_in_this - height @highest_in_all -= diff @highest_in_this = height end move_cursor_up(@started_from) @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 cursor_up_from_last_line = height - 1 - @started_from end if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render) @output.write "\e[0m" # clear character decorations end visual_lines.each_with_index do |line, index| Reline::IOGate.move_cursor_column(0) if line.nil? if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last # reaches the end of line if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? # 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) Reline::IOGate.move_cursor_column(0) end next end @output.write line if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last # A newline is automatically inserted if a character is rendered at eol on command prompt. @rest_height -= 1 if @rest_height > 0 end @output.flush if @first_prompt @first_prompt = false @pre_input_hook&.call end end unless visual_lines.empty? Reline::IOGate.erase_after_cursor Reline::IOGate.move_cursor_column(0) end if with_control # Just after rendring, so the cursor is on the last line. if finished? Reline::IOGate.move_cursor_column(0) else # Moves up from bottom of lines to the cursor position. move_cursor_up(cursor_up_from_last_line) # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line. Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end end height end private def modify_lines(before) return before if before.nil? || before.empty? || simplified_rendering? if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?) after.lines("\n").map { |l| l.chomp('') } else before end end private def show_menu scroll_down(@highest_in_all - @first_line_started_from) @rerender_all = true @menu_info.list.sort!.each do |item| Reline::IOGate.move_cursor_column(0) @output.write item @output.flush scroll_down(1) end scroll_down(@highest_in_all - 1) move_cursor_up(@highest_in_all - 1 - @first_line_started_from) end private def clear_screen_buffer(prompt, prompt_list, prompt_width) Reline::IOGate.clear_screen back = 0 modify_lines(whole_lines).each_with_index do |line, index| if @prompt_proc pr = prompt_list[index] height = render_partial(pr, calculate_width(pr), line, back, with_control: false) else height = render_partial(prompt, prompt_width, line, back, with_control: false) end if index < (@buffer_of_lines.size - 1) move_cursor_down(height) back += height end end move_cursor_up(back) move_cursor_down(@first_line_started_from + @started_from) @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end def editing_mode @config.editing_mode end private def menu(target, list) @menu_info = MenuInfo.new(target, list) end private def complete_internal_proc(list, is_menu) preposing, target, postposing = retrieve_completion_block list = list.select { |i| if i and not Encoding.compatible?(target.encoding, i.encoding) raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}" end if @config.completion_ignore_case i&.downcase&.start_with?(target.downcase) else i&.start_with?(target) end }.uniq if is_menu menu(target, list) return nil end completed = list.inject { |memo, item| begin memo_mbchars = memo.unicode_normalize.grapheme_clusters item_mbchars = item.unicode_normalize.grapheme_clusters rescue Encoding::CompatibilityError memo_mbchars = memo.grapheme_clusters item_mbchars = item.grapheme_clusters end size = [memo_mbchars.size, item_mbchars.size].min result = '' size.times do |i| if @config.completion_ignore_case if memo_mbchars[i].casecmp?(item_mbchars[i]) result << memo_mbchars[i] else break end else if memo_mbchars[i] == item_mbchars[i] result << memo_mbchars[i] else break end end end result } [target, preposing, completed, postposing] end private def complete(list, just_show_list = false) case @completion_state when CompletionState::NORMAL, CompletionState::JOURNEY @completion_state = CompletionState::COMPLETION when CompletionState::PERFECT_MATCH @dig_perfect_match_proc&.(@perfect_matched) end if just_show_list is_menu = true elsif @completion_state == CompletionState::MENU is_menu = true elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH is_menu = true else is_menu = false end result = complete_internal_proc(list, is_menu) if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH @completion_state = CompletionState::PERFECT_MATCH end return if result.nil? target, preposing, completed, postposing = result return if completed.nil? if target <= completed and (@completion_state == CompletionState::COMPLETION) if list.include?(completed) if list.one? @completion_state = CompletionState::PERFECT_MATCH else @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH end @perfect_matched = completed else @completion_state = CompletionState::MENU end if not just_show_list and target < completed @line = preposing + completed + completion_append_character.to_s + postposing line_to_pointer = preposing + completed + completion_append_character.to_s @cursor_max = calculate_width(@line) @cursor = calculate_width(line_to_pointer) @byte_pointer = line_to_pointer.bytesize end end end private def move_completed_list(list, direction) case @completion_state when CompletionState::NORMAL, CompletionState::COMPLETION, CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH @completion_state = CompletionState::JOURNEY result = retrieve_completion_block return if result.nil? preposing, target, postposing = result @completion_journey_data = CompletionJourneyData.new( preposing, postposing, [target] + list.select{ |item| item.start_with?(target) }, 0) @completion_state = CompletionState::JOURNEY else case direction when :up @completion_journey_data.pointer -= 1 if @completion_journey_data.pointer < 0 @completion_journey_data.pointer = @completion_journey_data.list.size - 1 end when :down @completion_journey_data.pointer += 1 if @completion_journey_data.pointer >= @completion_journey_data.list.size @completion_journey_data.pointer = 0 end end completed = @completion_journey_data.list[@completion_journey_data.pointer] @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing line_to_pointer = @completion_journey_data.preposing + completed @cursor_max = calculate_width(@line) @cursor = calculate_width(line_to_pointer) @byte_pointer = line_to_pointer.bytesize end end private def run_for_operators(key, method_symbol, &block) if @waiting_operator_proc if VI_MOTIONS.include?(method_symbol) old_cursor, old_byte_pointer = @cursor, @byte_pointer @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1 block.(true) unless @waiting_proc cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer @cursor, @byte_pointer = old_cursor, old_byte_pointer @waiting_operator_proc.(cursor_diff, byte_pointer_diff) else old_waiting_proc = @waiting_proc old_waiting_operator_proc = @waiting_operator_proc current_waiting_operator_proc = @waiting_operator_proc @waiting_proc = proc { |k| old_cursor, old_byte_pointer = @cursor, @byte_pointer old_waiting_proc.(k) cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer @cursor, @byte_pointer = old_cursor, old_byte_pointer current_waiting_operator_proc.(cursor_diff, byte_pointer_diff) @waiting_operator_proc = old_waiting_operator_proc } end else # Ignores operator when not motion is given. block.(false) end @waiting_operator_proc = nil @waiting_operator_vi_arg = nil @vi_arg = nil else block.(false) end end private def argumentable?(method_obj) method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg } end private def inclusive?(method_obj) # If a motion method with the keyword argument "inclusive" follows the # operator, it must contain the character at the cursor position. method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive } end def wrap_method_call(method_symbol, method_obj, key, with_operator = false) if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil? not_insertion = method_symbol != :ed_insert process_insert(force: not_insertion) end if @vi_arg and argumentable?(method_obj) if with_operator and inclusive?(method_obj) method_obj.(key, arg: @vi_arg, inclusive: true) else method_obj.(key, arg: @vi_arg) end else if with_operator and inclusive?(method_obj) method_obj.(key, inclusive: true) else method_obj.(key) end end end private def process_key(key, method_symbol) if method_symbol and respond_to?(method_symbol, true) method_obj = method(method_symbol) else method_obj = nil end if method_symbol and key.is_a?(Symbol) if @vi_arg and argumentable?(method_obj) run_for_operators(key, method_symbol) do |with_operator| wrap_method_call(method_symbol, method_obj, key, with_operator) end else wrap_method_call(method_symbol, method_obj, key) if method_obj end @kill_ring.process @vi_arg = nil elsif @vi_arg if key.chr =~ /[0-9]/ ed_argument_digit(key) else if argumentable?(method_obj) run_for_operators(key, method_symbol) do |with_operator| wrap_method_call(method_symbol, method_obj, key, with_operator) end elsif @waiting_proc @waiting_proc.(key) elsif method_obj wrap_method_call(method_symbol, method_obj, key) else ed_insert(key) unless @config.editing_mode_is?(:vi_command) end @kill_ring.process @vi_arg = nil end elsif @waiting_proc @waiting_proc.(key) @kill_ring.process elsif method_obj if method_symbol == :ed_argument_digit wrap_method_call(method_symbol, method_obj, key) else run_for_operators(key, method_symbol) do |with_operator| wrap_method_call(method_symbol, method_obj, key, with_operator) end end @kill_ring.process else ed_insert(key) unless @config.editing_mode_is?(:vi_command) end end private def normal_char(key) method_symbol = method_obj = nil if key.combined_char.is_a?(Symbol) process_key(key.combined_char, key.combined_char) return end @multibyte_buffer << key.combined_char if @multibyte_buffer.size > 1 if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding? process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil) @multibyte_buffer.clear else # invalid return end else # single byte return if key.char >= 128 # maybe, first byte of multi byte method_symbol = @config.editing_mode.get_method(key.combined_char) if key.with_meta and method_symbol == :ed_unassigned # split ESC + key method_symbol = @config.editing_mode.get_method("\e".ord) process_key("\e".ord, method_symbol) method_symbol = @config.editing_mode.get_method(key.char) process_key(key.char, method_symbol) else process_key(key.combined_char, method_symbol) end @multibyte_buffer.clear end if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer -= byte_size mbchar = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor -= width end end def input_key(key) @just_cursor_moving = nil if key.char.nil? if @first_char @line = nil end finish return end old_line = @line.dup @first_char = false completion_occurs = false if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord unless @config.disable_completion result = call_completion_proc if result.is_a?(Array) completion_occurs = true process_insert complete(result) end end elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) unless @config.disable_completion result = call_completion_proc if result.is_a?(Array) completion_occurs = true process_insert move_completed_list(result, "\C-p".ord == key.char ? :up : :down) end end elsif Symbol === key.char and respond_to?(key.char, true) process_key(key.char, key.char) else normal_char(key) end unless completion_occurs @completion_state = CompletionState::NORMAL end if not @in_pasting and @just_cursor_moving.nil? if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line @just_cursor_moving = true elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line @just_cursor_moving = true else @just_cursor_moving = false end else @just_cursor_moving = false end if @is_multiline and @auto_indent_proc and not simplified_rendering? process_auto_indent end end def call_completion_proc result = retrieve_completion_block(true) preposing, target, postposing = result if @completion_proc and target argnum = @completion_proc.parameters.inject(0) { |result, item| case item.first when :req, :opt result + 1 when :rest break 3 end } case argnum when 1 result = @completion_proc.(target) when 2 result = @completion_proc.(target, preposing) when 3..Float::INFINITY result = @completion_proc.(target, preposing, postposing) end end Reline.core.instance_variable_set(:@completion_quote_character, nil) result end private def process_auto_indent return if not @check_new_auto_indent and @previous_line_index # move cursor up or down if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index # Fix indent of a line when a newline is inserted to the next new_lines = whole_lines(index: @previous_line_index, line: @line) new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true) md = @line.match(/\A */) prev_indent = md[0].count(' ') @line = ' ' * new_indent + @line.lstrip new_indent = nil result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false) if result new_indent = result end if new_indent&.>= 0 @line = ' ' * new_indent + @line.lstrip end end if @previous_line_index new_lines = whole_lines(index: @previous_line_index, line: @line) else new_lines = whole_lines end new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent) new_indent = @cursor_max if new_indent&.> @cursor_max if new_indent&.>= 0 md = new_lines[@line_index].match(/\A */) prev_indent = md[0].count(' ') if @check_new_auto_indent @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip @cursor = new_indent @byte_pointer = new_indent else @line = ' ' * new_indent + @line.lstrip @cursor += new_indent - prev_indent @byte_pointer += new_indent - prev_indent end end @check_new_auto_indent = false end def retrieve_completion_block(set_completion_quote_character = false) if Reline.completer_word_break_characters.empty? word_break_regexp = nil else word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ end if Reline.completer_quote_characters.empty? quote_characters_regexp = nil else quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ end before = @line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil quote = nil closing_quote = nil escaped_quote = nil i = 0 while i < @byte_pointer do slice = @line.byteslice(i, @byte_pointer - i) unless slice.valid_encoding? i += 1 next end if quote and slice.start_with?(closing_quote) quote = nil i += 1 rest = nil elsif quote and slice.start_with?(escaped_quote) # skip i += 2 elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new " rest = $' quote = $& closing_quote = /(?!\\)#{Regexp.escape(quote)}/ escaped_quote = /\\#{Regexp.escape(quote)}/ i += 1 break_pointer = i - 1 elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 before = @line.byteslice(i, @byte_pointer - i) break_pointer = i else i += 1 end end postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) if rest preposing = @line.byteslice(0, break_pointer) target = rest if set_completion_quote_character and quote Reline.core.instance_variable_set(:@completion_quote_character, quote) if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote insert_text(quote) end end else preposing = '' if break_pointer preposing = @line.byteslice(0, break_pointer) else preposing = '' end target = before end if @is_multiline if @previous_line_index lines = whole_lines(index: @previous_line_index, line: @line) else lines = whole_lines end if @line_index > 0 preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing end if (lines.size - 1) > @line_index postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n") end end [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)] end def confirm_multiline_termination temp_buffer = @buffer_of_lines.dup if @previous_line_index and @line_index == (@buffer_of_lines.size - 1) temp_buffer[@previous_line_index] = @line else temp_buffer[@line_index] = @line end @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end def insert_text(text) width = calculate_width(text) if @cursor == @cursor_max @line += text else @line = byteinsert(@line, @byte_pointer, text) end @byte_pointer += text.bytesize @cursor += width @cursor_max += width end def delete_text(start = nil, length = nil) if start.nil? and length.nil? if @is_multiline if @buffer_of_lines.size == 1 @line&.clear @byte_pointer = 0 @cursor = 0 @cursor_max = 0 elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0 @buffer_of_lines.pop @line_index -= 1 @line = @buffer_of_lines[@line_index] @byte_pointer = 0 @cursor = 0 @cursor_max = calculate_width(@line) elsif @line_index < (@buffer_of_lines.size - 1) @buffer_of_lines.delete_at(@line_index) @line = @buffer_of_lines[@line_index] @byte_pointer = 0 @cursor = 0 @cursor_max = calculate_width(@line) end else @line&.clear @byte_pointer = 0 @cursor = 0 @cursor_max = 0 end elsif not start.nil? and not length.nil? if @line before = @line.byteslice(0, start) after = @line.byteslice(start + length, @line.bytesize) @line = before + after @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize str = @line.byteslice(0, @byte_pointer) @cursor = calculate_width(str) @cursor_max = calculate_width(@line) end elsif start.is_a?(Range) range = start first = range.first last = range.last last = @line.bytesize - 1 if last > @line.bytesize last += @line.bytesize if last < 0 first += @line.bytesize if first < 0 range = range.exclude_end? ? first...last : first..last @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding) @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize str = @line.byteslice(0, @byte_pointer) @cursor = calculate_width(str) @cursor_max = calculate_width(@line) else @line = @line.byteslice(0, start) @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize str = @line.byteslice(0, @byte_pointer) @cursor = calculate_width(str) @cursor_max = calculate_width(@line) end end def byte_pointer=(val) @byte_pointer = val str = @line.byteslice(0, @byte_pointer) @cursor = calculate_width(str) @cursor_max = calculate_width(@line) end def whole_lines(index: @line_index, line: @line) temp_lines = @buffer_of_lines.dup temp_lines[index] = line temp_lines end def whole_buffer if @buffer_of_lines.size == 1 and @line.nil? nil else if @previous_line_index whole_lines(index: @previous_line_index, line: @line).join("\n") else whole_lines.join("\n") end end end def finished? @finished end def finish @finished = true @rerender_all = true @config.reset end private def byteslice!(str, byte_pointer, size) new_str = str.byteslice(0, byte_pointer) new_str << str.byteslice(byte_pointer + size, str.bytesize) [new_str, str.byteslice(byte_pointer, size)] end private def byteinsert(str, byte_pointer, other) new_str = str.byteslice(0, byte_pointer) new_str << other new_str << str.byteslice(byte_pointer, str.bytesize) new_str end private def calculate_width(str, allow_escape_code = false) Reline::Unicode.calculate_width(str, allow_escape_code) end private def key_delete(key) if @config.editing_mode_is?(:vi_insert, :emacs) ed_delete_next_char(key) end end private def key_newline(key) if @is_multiline if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer @add_newline_to_end_of_buffer = true end next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) cursor_line = @line.byteslice(0, @byte_pointer) insert_new_line(cursor_line, next_line) @cursor = 0 @check_new_auto_indent = true unless @in_pasting end end private def ed_unassigned(key) end # do nothing private def process_insert(force: false) return if @continuous_insertion_buffer.empty? or (@in_pasting and not force) width = Reline::Unicode.calculate_width(@continuous_insertion_buffer) bytesize = @continuous_insertion_buffer.bytesize if @cursor == @cursor_max @line += @continuous_insertion_buffer else @line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer) end @byte_pointer += bytesize @cursor += width @cursor_max += width @continuous_insertion_buffer.clear end private def ed_insert(key) str = nil width = nil bytesize = nil if key.instance_of?(String) begin key.encode(Encoding::UTF_8) rescue Encoding::UndefinedConversionError return end str = key bytesize = key.bytesize else begin key.chr.encode(Encoding::UTF_8) rescue Encoding::UndefinedConversionError return end str = key.chr bytesize = 1 end if @in_pasting @continuous_insertion_buffer << str return elsif not @continuous_insertion_buffer.empty? process_insert end width = Reline::Unicode.get_mbchar_width(str) if @cursor == @cursor_max @line += str else @line = byteinsert(@line, @byte_pointer, str) end last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer += bytesize last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size) if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1 width = 0 end @cursor += width @cursor_max += width end alias_method :ed_digit, :ed_insert alias_method :self_insert, :ed_insert private def ed_quoted_insert(str, arg: 1) @waiting_proc = proc { |key| arg.times do if key == "\C-j".ord or key == "\C-m".ord key_newline(key) else ed_insert(key) end end @waiting_proc = nil } end alias_method :quoted_insert, :ed_quoted_insert private def ed_next_char(key, arg: 1) byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) if (@byte_pointer < @line.bytesize) mbchar = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor += width if width @byte_pointer += byte_size elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1 next_line = @buffer_of_lines[@line_index + 1] @cursor = 0 @byte_pointer = 0 @cursor_max = calculate_width(next_line) @previous_line_index = @line_index @line_index += 1 end arg -= 1 ed_next_char(key, arg: arg) if arg > 0 end alias_method :forward_char, :ed_next_char private def ed_prev_char(key, arg: 1) if @cursor > 0 byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer -= byte_size mbchar = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor -= width elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0 prev_line = @buffer_of_lines[@line_index - 1] @cursor = calculate_width(prev_line) @byte_pointer = prev_line.bytesize @cursor_max = calculate_width(prev_line) @previous_line_index = @line_index @line_index -= 1 end arg -= 1 ed_prev_char(key, arg: arg) if arg > 0 end alias_method :backward_char, :ed_prev_char private def vi_first_print(key) @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line) end private def ed_move_to_beg(key) @byte_pointer = @cursor = 0 end alias_method :beginning_of_line, :ed_move_to_beg private def ed_move_to_end(key) @byte_pointer = 0 @cursor = 0 byte_size = 0 while @byte_pointer < @line.bytesize byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) if byte_size > 0 mbchar = @line.byteslice(@byte_pointer, byte_size) @cursor += Reline::Unicode.get_mbchar_width(mbchar) end @byte_pointer += byte_size end end alias_method :end_of_line, :ed_move_to_end private def generate_searcher Fiber.new do |first_key| prev_search_key = first_key search_word = String.new(encoding: @encoding) multibyte_buf = String.new(encoding: 'ASCII-8BIT') last_hit = nil case first_key when "\C-r".ord prompt_name = 'reverse-i-search' when "\C-s".ord prompt_name = 'i-search' end loop do key = Fiber.yield(search_word) search_again = false case key when -1 # determined Reline.last_incremental_search = search_word break when "\C-h".ord, "\C-?".ord grapheme_clusters = search_word.grapheme_clusters if grapheme_clusters.size > 0 grapheme_clusters.pop search_word = grapheme_clusters.join end when "\C-r".ord, "\C-s".ord search_again = true if prev_search_key == key prev_search_key = key else multibyte_buf << key if multibyte_buf.dup.force_encoding(@encoding).valid_encoding? search_word << multibyte_buf.dup.force_encoding(@encoding) multibyte_buf.clear end end hit = nil if not search_word.empty? and @line_backup_in_history&.include?(search_word) @history_pointer = nil hit = @line_backup_in_history else if search_again if search_word.empty? and Reline.last_incremental_search search_word = Reline.last_incremental_search end if @history_pointer case prev_search_key when "\C-r".ord history_pointer_base = 0 history = Reline::HISTORY[0..(@history_pointer - 1)] when "\C-s".ord history_pointer_base = @history_pointer + 1 history = Reline::HISTORY[(@history_pointer + 1)..-1] end else history_pointer_base = 0 history = Reline::HISTORY end elsif @history_pointer case prev_search_key when "\C-r".ord history_pointer_base = 0 history = Reline::HISTORY[0..@history_pointer] when "\C-s".ord history_pointer_base = @history_pointer history = Reline::HISTORY[@history_pointer..-1] end else history_pointer_base = 0 history = Reline::HISTORY end case prev_search_key when "\C-r".ord hit_index = history.rindex { |item| item.include?(search_word) } when "\C-s".ord hit_index = history.index { |item| item.include?(search_word) } end if hit_index @history_pointer = history_pointer_base + hit_index hit = Reline::HISTORY[@history_pointer] end end case prev_search_key when "\C-r".ord prompt_name = 'reverse-i-search' when "\C-s".ord prompt_name = 'i-search' end if hit if @is_multiline @buffer_of_lines = hit.split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true @searching_prompt = "(%s)`%s'" % [prompt_name, search_word] else @line = hit @searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit] end last_hit = hit else if @is_multiline @rerender_all = true @searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word] else @searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit] end end end end end private def incremental_search_history(key) unless @history_pointer if @is_multiline @line_backup_in_history = whole_buffer else @line_backup_in_history = @line end end searcher = generate_searcher searcher.resume(key) @searching_prompt = "(reverse-i-search)`': " termination_keys = ["\C-j".ord] termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators @waiting_proc = ->(k) { case k when *termination_keys if @history_pointer buffer = Reline::HISTORY[@history_pointer] else buffer = @line_backup_in_history end if @is_multiline @buffer_of_lines = buffer.split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true else @line = buffer end @searching_prompt = nil @waiting_proc = nil @cursor_max = calculate_width(@line) @cursor = @byte_pointer = 0 @rerender_all = true @cached_prompt_list = nil searcher.resume(-1) when "\C-g".ord if @is_multiline @buffer_of_lines = @line_backup_in_history.split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true else @line = @line_backup_in_history end @history_pointer = nil @searching_prompt = nil @waiting_proc = nil @line_backup_in_history = nil @cursor_max = calculate_width(@line) @cursor = @byte_pointer = 0 @rerender_all = true else chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT) if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord searcher.resume(k) else if @history_pointer line = Reline::HISTORY[@history_pointer] else line = @line_backup_in_history end if @is_multiline @line_backup_in_history = whole_buffer @buffer_of_lines = line.split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true else @line_backup_in_history = @line @line = line end @searching_prompt = nil @waiting_proc = nil @cursor_max = calculate_width(@line) @cursor = @byte_pointer = 0 @rerender_all = true @cached_prompt_list = nil searcher.resume(-1) end end } end private def vi_search_prev(key) incremental_search_history(key) end alias_method :reverse_search_history, :vi_search_prev private def vi_search_next(key) incremental_search_history(key) end 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[@line_index] @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[@line_index] @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 @previous_line_index = @line_index @line_index -= 1 return end if Reline::HISTORY.empty? return end if @history_pointer.nil? @history_pointer = Reline::HISTORY.size - 1 if @is_multiline @line_backup_in_history = whole_buffer @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true else @line_backup_in_history = @line @line = Reline::HISTORY[@history_pointer] end elsif @history_pointer.zero? return else if @is_multiline Reline::HISTORY[@history_pointer] = whole_buffer @history_pointer -= 1 @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = @buffer_of_lines.size - 1 @line = @buffer_of_lines.last @rerender_all = true else Reline::HISTORY[@history_pointer] = @line @history_pointer -= 1 @line = Reline::HISTORY[@history_pointer] end end if @config.editing_mode_is?(:emacs, :vi_insert) @cursor_max = @cursor = calculate_width(@line) @byte_pointer = @line.bytesize elsif @config.editing_mode_is?(:vi_command) @byte_pointer = @cursor = 0 @cursor_max = calculate_width(@line) end arg -= 1 ed_prev_history(key, arg: arg) if arg > 0 end private def ed_next_history(key, arg: 1) if @is_multiline and @line_index < (@buffer_of_lines.size - 1) @previous_line_index = @line_index @line_index += 1 return end if @history_pointer.nil? return elsif @history_pointer == (Reline::HISTORY.size - 1) if @is_multiline @history_pointer = nil @buffer_of_lines = @line_backup_in_history.split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = 0 @line = @buffer_of_lines.first @rerender_all = true else @history_pointer = nil @line = @line_backup_in_history end else if @is_multiline Reline::HISTORY[@history_pointer] = whole_buffer @history_pointer += 1 @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = 0 @line = @buffer_of_lines.first @rerender_all = true else Reline::HISTORY[@history_pointer] = @line @history_pointer += 1 @line = Reline::HISTORY[@history_pointer] end end @line = '' unless @line if @config.editing_mode_is?(:emacs, :vi_insert) @cursor_max = @cursor = calculate_width(@line) @byte_pointer = @line.bytesize elsif @config.editing_mode_is?(:vi_command) @byte_pointer = @cursor = 0 @cursor_max = calculate_width(@line) end arg -= 1 ed_next_history(key, arg: arg) if arg > 0 end private def ed_newline(key) process_insert(force: true) if @is_multiline if @config.editing_mode_is?(:vi_command) if @line_index < (@buffer_of_lines.size - 1) ed_next_history(key) # means cursor down else # should check confirm_multiline_termination to finish? finish end else if @line_index == (@buffer_of_lines.size - 1) if confirm_multiline_termination finish else key_newline(key) end else # should check confirm_multiline_termination to finish? @previous_line_index = @line_index @line_index = @buffer_of_lines.size - 1 finish end end else if @history_pointer Reline::HISTORY[@history_pointer] = @line @history_pointer = nil end finish end end private def em_delete_prev_char(key) if @is_multiline and @cursor == 0 and @line_index > 0 @buffer_of_lines[@line_index] = @line @cursor = calculate_width(@buffer_of_lines[@line_index - 1]) @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) @line_index -= 1 @line = @buffer_of_lines[@line_index] @cursor_max = calculate_width(@line) @rerender_all = true elsif @cursor > 0 byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer -= byte_size @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor -= width @cursor_max -= width end end alias_method :backward_delete_char, :em_delete_prev_char private def ed_kill_line(key) if @line.bytesize > @byte_pointer @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer) @byte_pointer = @line.bytesize @cursor = @cursor_max = calculate_width(@line) @kill_ring.append(deleted) elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 @cursor = calculate_width(@line) @byte_pointer = @line.bytesize @line += @buffer_of_lines.delete_at(@line_index + 1) @cursor_max = calculate_width(@line) @buffer_of_lines[@line_index] = @line @rerender_all = true @rest_height += 1 end end private def em_kill_line(key) if @byte_pointer > 0 @line, deleted = byteslice!(@line, 0, @byte_pointer) @byte_pointer = 0 @kill_ring.append(deleted, true) @cursor_max = calculate_width(@line) @cursor = 0 end end alias_method :kill_line, :em_kill_line private def em_delete(key) if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) @line = nil if @buffer_of_lines.size > 1 scroll_down(@highest_in_all - @first_line_started_from) end Reline::IOGate.move_cursor_column(0) @eof = true finish elsif @byte_pointer < @line.bytesize splitted_last = @line.byteslice(@byte_pointer, @line.bytesize) mbchar = splitted_last.grapheme_clusters.first width = Reline::Unicode.get_mbchar_width(mbchar) @cursor_max -= width @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize) elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 @cursor = calculate_width(@line) @byte_pointer = @line.bytesize @line += @buffer_of_lines.delete_at(@line_index + 1) @cursor_max = calculate_width(@line) @buffer_of_lines[@line_index] = @line @rerender_all = true @rest_height += 1 end end alias_method :delete_char, :em_delete private def em_delete_or_list(key) if @line.empty? or @byte_pointer < @line.bytesize em_delete(key) else # show completed list result = call_completion_proc if result.is_a?(Array) complete(result, true) end end end alias_method :delete_char_or_list, :em_delete_or_list private def em_yank(key) yanked = @kill_ring.yank if yanked @line = byteinsert(@line, @byte_pointer, yanked) yanked_width = calculate_width(yanked) @cursor += yanked_width @cursor_max += yanked_width @byte_pointer += yanked.bytesize end end alias_method :yank, :em_yank private def em_yank_pop(key) yanked, prev_yank = @kill_ring.yank_pop if yanked prev_yank_width = calculate_width(prev_yank) @cursor -= prev_yank_width @cursor_max -= prev_yank_width @byte_pointer -= prev_yank.bytesize @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize) @line = byteinsert(@line, @byte_pointer, yanked) yanked_width = calculate_width(yanked) @cursor += yanked_width @cursor_max += yanked_width @byte_pointer += yanked.bytesize end end alias_method :yank_pop, :em_yank_pop private def ed_clear_screen(key) @cleared = true end alias_method :clear_screen, :ed_clear_screen private def em_next_word(key) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) @byte_pointer += byte_size @cursor += width end end alias_method :forward_word, :em_next_word private def ed_prev_word(key) if @byte_pointer > 0 byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) @byte_pointer -= byte_size @cursor -= width end end alias_method :backward_word, :ed_prev_word private def em_delete_next_word(key) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) @line, word = byteslice!(@line, @byte_pointer, byte_size) @kill_ring.append(word) @cursor_max -= width end end private def ed_delete_prev_word(key) if @byte_pointer > 0 byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size) @kill_ring.append(word, true) @byte_pointer -= byte_size @cursor -= width @cursor_max -= width end end private def ed_transpose_chars(key) if @byte_pointer > 0 if @cursor_max > @cursor byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) mbchar = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor += width @byte_pointer += byte_size end back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) if (@byte_pointer - back1_byte_size) > 0 back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size) back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size) @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar) end end end alias_method :transpose_chars, :ed_transpose_chars private def ed_transpose_words(key) left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer) before = @line.byteslice(0, left_word_start) left_word = @line.byteslice(left_word_start, middle_start - left_word_start) middle = @line.byteslice(middle_start, right_word_start - middle_start) right_word = @line.byteslice(right_word_start, after_start - right_word_start) after = @line.byteslice(after_start, @line.bytesize - after_start) return if left_word.empty? or right_word.empty? @line = before + right_word + middle + left_word + after from_head_to_left_word = before + right_word + middle + left_word @byte_pointer = from_head_to_left_word.bytesize @cursor = calculate_width(from_head_to_left_word) end alias_method :transpose_words, :ed_transpose_words private def em_capitol_case(key) if @line.bytesize > @byte_pointer byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer) before = @line.byteslice(0, @byte_pointer) after = @line.byteslice((@byte_pointer + byte_size)..-1) @line = before + new_str + after @byte_pointer += new_str.bytesize @cursor += calculate_width(new_str) end end alias_method :capitalize_word, :em_capitol_case private def em_lower_case(key) if @line.bytesize > @byte_pointer byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar }.join rest = @line.byteslice((@byte_pointer + byte_size)..-1) @line = @line.byteslice(0, @byte_pointer) + part @byte_pointer = @line.bytesize @cursor = calculate_width(@line) @cursor_max = @cursor + calculate_width(rest) @line += rest end end alias_method :downcase_word, :em_lower_case private def em_upper_case(key) if @line.bytesize > @byte_pointer byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar }.join rest = @line.byteslice((@byte_pointer + byte_size)..-1) @line = @line.byteslice(0, @byte_pointer) + part @byte_pointer = @line.bytesize @cursor = calculate_width(@line) @cursor_max = @cursor + calculate_width(rest) @line += rest end end alias_method :upcase_word, :em_upper_case private def em_kill_region(key) if @byte_pointer > 0 byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer) @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size) @byte_pointer -= byte_size @cursor -= width @cursor_max -= width @kill_ring.append(deleted, true) end end alias_method :unix_word_rubout, :em_kill_region private def copy_for_vi(text) if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command) @vi_clipboard = text end end private def vi_insert(key) @config.editing_mode = :vi_insert end private def vi_add(key) @config.editing_mode = :vi_insert ed_next_char(key) end private def vi_command_mode(key) ed_prev_char(key) @config.editing_mode = :vi_command end alias_method :vi_movement_mode, :vi_command_mode private def vi_next_word(key, arg: 1) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces) @byte_pointer += byte_size @cursor += width end arg -= 1 vi_next_word(key, arg: arg) if arg > 0 end private def vi_prev_word(key, arg: 1) if @byte_pointer > 0 byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer) @byte_pointer -= byte_size @cursor -= width end arg -= 1 vi_prev_word(key, arg: arg) if arg > 0 end private def vi_end_word(key, arg: 1, inclusive: false) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer) @byte_pointer += byte_size @cursor += width end arg -= 1 if inclusive and arg.zero? byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) if byte_size > 0 c = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size @cursor += width end end vi_end_word(key, arg: arg) if arg > 0 end private def vi_next_big_word(key, arg: 1) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer) @byte_pointer += byte_size @cursor += width end arg -= 1 vi_next_big_word(key, arg: arg) if arg > 0 end private def vi_prev_big_word(key, arg: 1) if @byte_pointer > 0 byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer) @byte_pointer -= byte_size @cursor -= width end arg -= 1 vi_prev_big_word(key, arg: arg) if arg > 0 end private def vi_end_big_word(key, arg: 1, inclusive: false) if @line.bytesize > @byte_pointer byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer) @byte_pointer += byte_size @cursor += width end arg -= 1 if inclusive and arg.zero? byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) if byte_size > 0 c = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size @cursor += width end end vi_end_big_word(key, arg: arg) if arg > 0 end private def vi_delete_prev_char(key) if @is_multiline and @cursor == 0 and @line_index > 0 @buffer_of_lines[@line_index] = @line @cursor = calculate_width(@buffer_of_lines[@line_index - 1]) @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) @line_index -= 1 @line = @buffer_of_lines[@line_index] @cursor_max = calculate_width(@line) @rerender_all = true elsif @cursor > 0 byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer -= byte_size @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor -= width @cursor_max -= width end end private def vi_insert_at_bol(key) ed_move_to_beg(key) @config.editing_mode = :vi_insert end private def vi_add_at_eol(key) ed_move_to_end(key) @config.editing_mode = :vi_insert end private def ed_delete_prev_char(key, arg: 1) deleted = '' arg.times do if @cursor > 0 byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) @byte_pointer -= byte_size @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) deleted.prepend(mbchar) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor -= width @cursor_max -= width end end copy_for_vi(deleted) end private def vi_zero(key) @byte_pointer = 0 @cursor = 0 end private def vi_change_meta(key, arg: 1) @drop_terminate_spaces = true @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| if byte_pointer_diff > 0 @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) elsif byte_pointer_diff < 0 @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) end copy_for_vi(cut) @cursor += cursor_diff if cursor_diff < 0 @cursor_max -= cursor_diff.abs @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 @config.editing_mode = :vi_insert @drop_terminate_spaces = false } @waiting_operator_vi_arg = arg end private def vi_delete_meta(key, arg: 1) @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| if byte_pointer_diff > 0 @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) elsif byte_pointer_diff < 0 @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) end copy_for_vi(cut) @cursor += cursor_diff if cursor_diff < 0 @cursor_max -= cursor_diff.abs @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 } @waiting_operator_vi_arg = arg end private def vi_yank(key, arg: 1) @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| if byte_pointer_diff > 0 cut = @line.byteslice(@byte_pointer, byte_pointer_diff) elsif byte_pointer_diff < 0 cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) end copy_for_vi(cut) } @waiting_operator_vi_arg = arg end private def vi_list_or_eof(key) if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) @line = nil if @buffer_of_lines.size > 1 scroll_down(@highest_in_all - @first_line_started_from) end Reline::IOGate.move_cursor_column(0) @eof = true finish else ed_newline(key) end end alias_method :vi_end_of_transmission, :vi_list_or_eof alias_method :vi_eof_maybe, :vi_list_or_eof private def ed_delete_next_char(key, arg: 1) byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) unless @line.empty? || byte_size == 0 @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) copy_for_vi(mbchar) width = Reline::Unicode.get_mbchar_width(mbchar) @cursor_max -= width if @cursor > 0 and @cursor >= @cursor_max byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size) width = Reline::Unicode.get_mbchar_width(mbchar) @byte_pointer -= byte_size @cursor -= width end end arg -= 1 ed_delete_next_char(key, arg: arg) if arg > 0 end private def vi_to_history_line(key) if Reline::HISTORY.empty? return end if @history_pointer.nil? @history_pointer = 0 @line_backup_in_history = @line @line = Reline::HISTORY[@history_pointer] @cursor_max = calculate_width(@line) @cursor = 0 @byte_pointer = 0 elsif @history_pointer.zero? return else Reline::HISTORY[@history_pointer] = @line @history_pointer = 0 @line = Reline::HISTORY[@history_pointer] @cursor_max = calculate_width(@line) @cursor = 0 @byte_pointer = 0 end end private def vi_histedit(key) path = Tempfile.open { |fp| if @is_multiline fp.write whole_lines.join("\n") else fp.write @line end fp.path } system("#{ENV['EDITOR']} #{path}") if @is_multiline @buffer_of_lines = File.read(path).split("\n") @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? @line_index = 0 @line = @buffer_of_lines[@line_index] @rerender_all = true else @line = File.read(path) end finish end private def vi_paste_prev(key, arg: 1) if @vi_clipboard.size > 0 @line = byteinsert(@line, @byte_pointer, @vi_clipboard) @cursor_max += calculate_width(@vi_clipboard) cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join @cursor += calculate_width(cursor_point) @byte_pointer += cursor_point.bytesize end arg -= 1 vi_paste_prev(key, arg: arg) if arg > 0 end private def vi_paste_next(key, arg: 1) if @vi_clipboard.size > 0 byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard) @cursor_max += calculate_width(@vi_clipboard) @cursor += calculate_width(@vi_clipboard) @byte_pointer += @vi_clipboard.bytesize end arg -= 1 vi_paste_next(key, arg: arg) if arg > 0 end private def ed_argument_digit(key) if @vi_arg.nil? unless key.chr.to_i.zero? @vi_arg = key.chr.to_i end else @vi_arg = @vi_arg * 10 + key.chr.to_i end end private def vi_to_column(key, arg: 0) @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc| # total has [byte_size, cursor] mbchar_width = Reline::Unicode.get_mbchar_width(gc) if (total.last + mbchar_width) >= arg break total elsif (total.last + mbchar_width) >= @cursor_max break total else total = [total.first + gc.bytesize, total.last + mbchar_width] total end } end private def vi_replace_char(key, arg: 1) @waiting_proc = ->(k) { if arg == 1 byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) before = @line.byteslice(0, @byte_pointer) remaining_point = @byte_pointer + byte_size after = @line.byteslice(remaining_point, @line.bytesize - remaining_point) @line = before + k.chr + after @cursor_max = calculate_width(@line) @waiting_proc = nil elsif arg > 1 byte_size = 0 arg.times do byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size) end before = @line.byteslice(0, @byte_pointer) remaining_point = @byte_pointer + byte_size after = @line.byteslice(remaining_point, @line.bytesize - remaining_point) replaced = k.chr * arg @line = before + replaced + after @byte_pointer += replaced.bytesize @cursor += calculate_width(replaced) @cursor_max = calculate_width(@line) @waiting_proc = nil end } end private def vi_next_char(key, arg: 1, inclusive: false) @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) } end private def vi_to_next_char(key, arg: 1, inclusive: false) @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) } end private def search_next_char(key, arg, need_prev_char: false, inclusive: false) if key.instance_of?(String) inputed_char = key else inputed_char = key.chr end prev_total = nil total = nil found = false @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point width = Reline::Unicode.get_mbchar_width(mbchar) total = [mbchar.bytesize, width] else if inputed_char == mbchar arg -= 1 if arg.zero? found = true break end end width = Reline::Unicode.get_mbchar_width(mbchar) prev_total = total total = [total.first + mbchar.bytesize, total.last + width] end end if not need_prev_char and found and total byte_size, width = total @byte_pointer += byte_size @cursor += width elsif need_prev_char and found and prev_total byte_size, width = prev_total @byte_pointer += byte_size @cursor += width end if inclusive byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) if byte_size > 0 c = @line.byteslice(@byte_pointer, byte_size) width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size @cursor += width end end @waiting_proc = nil end private def vi_prev_char(key, arg: 1) @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) } end private def vi_to_prev_char(key, arg: 1) @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) } end private def search_prev_char(key, arg, need_next_char = false) if key.instance_of?(String) inputed_char = key else inputed_char = key.chr end prev_total = nil total = nil found = false @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point width = Reline::Unicode.get_mbchar_width(mbchar) total = [mbchar.bytesize, width] else if inputed_char == mbchar arg -= 1 if arg.zero? found = true break end end width = Reline::Unicode.get_mbchar_width(mbchar) prev_total = total total = [total.first + mbchar.bytesize, total.last + width] end end if not need_next_char and found and total byte_size, width = total @byte_pointer -= byte_size @cursor -= width elsif need_next_char and found and prev_total byte_size, width = prev_total @byte_pointer -= byte_size @cursor -= width end @waiting_proc = nil end private def vi_join_lines(key, arg: 1) if @is_multiline and @buffer_of_lines.size > @line_index + 1 @cursor = calculate_width(@line) @byte_pointer = @line.bytesize @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip @cursor_max = calculate_width(@line) @buffer_of_lines[@line_index] = @line @rerender_all = true @rest_height += 1 end arg -= 1 vi_join_lines(key, arg: arg) if arg > 0 end private def em_set_mark(key) @mark_pointer = [@byte_pointer, @line_index] end alias_method :set_mark, :em_set_mark private def em_exchange_mark(key) return unless @mark_pointer new_pointer = [@byte_pointer, @line_index] @previous_line_index = @line_index @byte_pointer, @line_index = @mark_pointer @cursor = calculate_width(@line.byteslice(0, @byte_pointer)) @cursor_max = calculate_width(@line) @mark_pointer = new_pointer end alias_method :exchange_point_and_mark, :em_exchange_mark end