diff options
Diffstat (limited to 'lib/reline/line_editor.rb')
-rw-r--r-- | lib/reline/line_editor.rb | 2828 |
1 files changed, 1040 insertions, 1788 deletions
diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index e8b83e2a6e..23ece60220 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -4,9 +4,7 @@ require 'reline/unicode' require 'tempfile' class Reline::LineEditor - # TODO: undo # TODO: Use "private alias_method" idiom after drop Ruby 2.5. - attr_reader :line attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc attr_accessor :completion_proc @@ -14,7 +12,6 @@ class Reline::LineEditor 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 @@ -35,99 +32,93 @@ class Reline::LineEditor 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) + RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true) + + CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer) + + class MenuInfo + attr_reader :list + + def initialize(list) + @list = list + end + + def lines(screen_width) + return [] if @list.empty? + + list = @list.sort + sizes = list.map { |item| Reline::Unicode.calculate_width(item) } + item_width = sizes.max + 2 + num_cols = [screen_width / item_width, 1].max + num_rows = list.size.fdiv(num_cols).ceil + list_with_padding = list.zip(sizes).map { |item, size| item + ' ' * (item_width - size) } + aligned = (list_with_padding + [nil] * (num_rows * num_cols - list_with_padding.size)).each_slice(num_rows).to_a.transpose + aligned.map do |row| + row.join.rstrip + end + end + end - PROMPT_LIST_CACHE_TIMEOUT = 0.5 MINIMUM_SCROLLBAR_HEIGHT = 1 def initialize(config, encoding) @config = config @completion_append_character = '' + @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset reset_variables(encoding: encoding) end - def set_pasting_state(in_pasting) - @in_pasting = in_pasting + def io_gate + Reline::IOGate 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 + def set_pasting_state(in_pasting) + # While pasting, text to be inserted is stored to @continuous_insertion_buffer. + # After pasting, this buffer should be force inserted. + process_insert(force: true) if @in_pasting && !in_pasting + @in_pasting = in_pasting 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 + @config.vi_cmd_mode_string elsif @config.editing_mode_is?(:vi_insert) - mode_string = @config.vi_ins_mode_string + @config.vi_ins_mode_string elsif @config.editing_mode_is?(:emacs) - mode_string = @config.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, force_recalc: false) + private def check_multiline_prompt(buffer, mode_string) 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? && !force_recalc + if !@is_multiline 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).map { |pr| pr.gsub("\n", "\\n") } - @prompt_cache_time = Time.now.to_f - end + [prompt] + [''] * (buffer.size - 1) + elsif @prompt_proc + prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") } 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? @@ -137,24 +128,17 @@ class Reline::LineEditor prompt_list << prompt_list.last end end - prompt_width = calculate_width(prompt, true) - [prompt, prompt_width, prompt_list] + 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] + [prompt] * buffer.size 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) - Reline::IOGate.set_winch_handler do - @resized = true - end + @rendered_screen.base_y = Reline::IOGate.cursor_pos.y if ENV.key?('RELINE_ALT_SCROLLBAR') @full_block = '::' @upper_half_block = "''" @@ -178,67 +162,53 @@ class Reline::LineEditor end end - def resize + def handle_signal + handle_interrupted + handle_resized + end + + private def handle_resized return unless @resized - @resized = false - @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 + @resized = false + scroll_into_view + Reline::IOGate.move_cursor_up @rendered_screen.cursor_y + @rendered_screen.base_y = Reline::IOGate.cursor_pos.y + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + render_differential + end + + private def handle_interrupted + return unless @interrupted + + @interrupted = false + clear_dialogs + scrolldown = render_differential + Reline::IOGate.scroll_down scrolldown + Reline::IOGate.move_cursor_column 0 + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + case @old_trap + when 'DEFAULT', 'SYSTEM_DEFAULT' + raise Interrupt + when 'IGNORE' + # Do nothing + when 'EXIT' + exit else - back = 0 - new_buffer = whole_lines - prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer) - 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 + @old_trap.call if @old_trap.respond_to?(:call) end end def set_signal_handlers - @old_trap = Signal.trap('INT') { - clear_dialog(0) - 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) - case @old_trap - when 'DEFAULT', 'SYSTEM_DEFAULT' - raise Interrupt - when 'IGNORE' - # Do nothing - when 'EXIT' - exit - else - @old_trap.call if @old_trap.respond_to?(:call) - end - } + Reline::IOGate.set_winch_handler do + @resized = true + end + @old_trap = Signal.trap('INT') do + @interrupted = true + end end def finalize @@ -255,56 +225,43 @@ class Reline::LineEditor @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 + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + @completion_journey_state = 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 + @just_cursor_moving = false @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) - @scroll_partial_screen = nil - @prev_mode_string = nil + @scroll_partial_screen = 0 @drop_terminate_spaces = false @in_pasting = false @auto_indent_proc = nil @dialogs = [] - @previous_rendered_dialog_y = 0 - @last_key = nil + @interrupted = false @resized = false + @cache = {} + @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0) + @past_lines = [] + @undoing = false 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 + @cache.clear @line_backup_in_history = nil @multibyte_buffer = String.new(encoding: 'ASCII-8BIT') - @check_new_auto_indent = false end def multiline_on @@ -315,68 +272,44 @@ class Reline::LineEditor @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 + @buffer_of_lines[@line_index] = cursor_line @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 + @byte_pointer = 0 + if @auto_indent_proc && !@in_pasting + if next_line.empty? + ( + # For compatibility, use this calculation instead of just `process_auto_indent @line_index - 1, cursor_dependent: false` + indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true) + indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false) + indent = indent2 || indent1 + @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A\s*/, '') + ) + process_auto_indent @line_index, add_newline: true + else + process_auto_indent @line_index - 1, cursor_dependent: false + process_auto_indent @line_index, add_newline: true # Need for compatibility + process_auto_indent @line_index, cursor_dependent: false + end 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 + private def split_by_width(str, max_width, offset: 0) + Reline::Unicode.split_by_width(str, max_width, @encoding, offset: offset) 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 + def current_byte_pointer_cursor + calculate_width(current_line.byteslice(0, @byte_pointer)) end - private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true) + private def calculate_nearest_cursor(cursor) + line_to_calc = current_line new_cursor_max = calculate_width(line_to_calc) new_cursor = 0 new_byte_pointer = 0 height = 1 - max_width = @screen_size.last + max_width = screen_width 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 @@ -402,113 +335,242 @@ class Reline::LineEditor 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] + @byte_pointer = new_byte_pointer + end + + def with_cache(key, *deps) + cached_deps, value = @cache[key] + if cached_deps != deps + @cache[key] = [deps, value = yield(*deps, cached_deps, value)] end + value end - def rerender_all - @rerender_all = true - process_insert(force: true) - rerender + def modified_lines + with_cache(__method__, whole_lines, finished?) do |whole, complete| + modify_lines(whole, complete) + end end - def rerender - return if @line.nil? - if @menu_info - scroll_down(@highest_in_all - @first_line_started_from) - @rerender_all = true + def prompt_list + with_cache(__method__, whole_lines, check_mode_string, @vi_arg, @searching_prompt) do |lines, mode_string| + check_multiline_prompt(lines, mode_string) end - if @menu_info - show_menu - @menu_info = nil - end - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines) - cursor_column = (prompt_width + @cursor) % @screen_size.last - if @cleared - clear_screen_buffer(prompt, prompt_list, prompt_width) - @cleared = false - return + end + + def screen_height + @screen_size.first + end + + def screen_width + @screen_size.last + end + + def screen_scroll_top + @scroll_partial_screen + end + + def wrapped_prompt_and_input_lines + with_cache(__method__, @buffer_of_lines.size, modified_lines, prompt_list, screen_width) do |n, lines, prompts, width, prev_cache_key, cached_value| + prev_n, prev_lines, prev_prompts, prev_width = prev_cache_key + cached_wraps = {} + if prev_width == width + prev_n.times do |i| + cached_wraps[[prev_prompts[i], prev_lines[i]]] = cached_value[i] + end + end + + n.times.map do |i| + prompt = prompts[i] || '' + line = lines[i] || '' + if (cached = cached_wraps[[prompt, line]]) + next cached + end + *wrapped_prompts, code_line_prompt = split_by_width(prompt, width).first.compact + wrapped_lines = split_by_width(line, width, offset: calculate_width(code_line_prompt, true)).first.compact + wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] } + end 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 - new_lines = whole_lines - prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines) - modify_lines(new_lines).each_with_index do |line, index| - @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\r\n" - Reline::IOGate.erase_after_cursor - end - @output.flush - clear_dialog(cursor_column) - return + end + + def calculate_overlay_levels(overlay_levels) + levels = [] + overlay_levels.each do |x, w, l| + levels.fill(l, x, w) end - new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) - rendered = false - if @add_newline_to_end_of_buffer - clear_dialog_with_trap_key(cursor_column) - rerender_added_newline(prompt, prompt_width, prompt_list) - @add_newline_to_end_of_buffer = false - else - if @just_cursor_moving and not @rerender_all - clear_dialog_with_trap_key(cursor_column) - rendered = just_move_cursor - @just_cursor_moving = false - return - elsif @previous_line_index or new_highest_in_this != @highest_in_this - clear_dialog_with_trap_key(cursor_column) - rerender_changed_current_line - @previous_line_index = nil - rendered = true - elsif @rerender_all - rerender_all_lines - @rerender_all = false - rendered = true + levels + end + + def render_line_differential(old_items, new_items) + old_levels = calculate_overlay_levels(old_items.zip(new_items).each_with_index.map {|((x, w, c), (nx, _nw, nc)), i| [x, w, c == nc && x == nx ? i : -1] if x }.compact) + new_levels = calculate_overlay_levels(new_items.each_with_index.map { |(x, w), i| [x, w, i] if x }.compact).take(screen_width) + base_x = 0 + new_levels.zip(old_levels).chunk { |n, o| n == o ? :skip : n || :blank }.each do |level, chunk| + width = chunk.size + if level == :skip + # do nothing + elsif level == :blank + Reline::IOGate.move_cursor_column base_x + @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" else + x, w, content = new_items[level] + cover_begin = base_x != 0 && new_levels[base_x - 1] == level + cover_end = new_levels[base_x + width] == level + pos = 0 + unless x == base_x && w == width + content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true) + end + Reline::IOGate.move_cursor_column x + pos + @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}" end + base_x += width end - if @is_multiline - if finished? - # Always rerender on finish because output_modifier_proc may return a different output. - new_lines = whole_lines - line = modify_lines(new_lines)[@line_index] - clear_dialog(cursor_column) - prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines) - 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 - else - if not rendered and not @in_pasting - line = modify_lines(whole_lines)[@line_index] - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines) - render_partial(prompt, prompt_width, line, @first_line_started_from) - end - render_dialog(cursor_column) + if old_levels.size > new_levels.size + Reline::IOGate.move_cursor_column new_levels.size + Reline::IOGate.erase_after_cursor + end + end + + # Calculate cursor position in word wrapped content. + def wrapped_cursor_position + prompt_width = calculate_width(prompt_list[@line_index], true) + line_before_cursor = whole_lines[@line_index].byteslice(0, @byte_pointer) + wrapped_line_before_cursor = split_by_width(' ' * prompt_width + line_before_cursor, screen_width).first.compact + wrapped_cursor_y = wrapped_prompt_and_input_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1 + wrapped_cursor_x = calculate_width(wrapped_line_before_cursor.last) + [wrapped_cursor_x, wrapped_cursor_y] + end + + def clear_dialogs + @dialogs.each do |dialog| + dialog.contents = nil + dialog.trap_key = nil + end + end + + def update_dialogs(key = nil) + wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + @dialogs.each do |dialog| + dialog.trap_key = nil + update_each_dialog(dialog, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top, key) + end + end + + def render_finished + clear_rendered_lines + render_full_content + end + + def clear_rendered_lines + Reline::IOGate.move_cursor_up @rendered_screen.cursor_y + Reline::IOGate.move_cursor_column 0 + + num_lines = @rendered_screen.lines.size + return unless num_lines && num_lines >= 1 + + Reline::IOGate.move_cursor_down num_lines - 1 + (num_lines - 1).times do + Reline::IOGate.erase_after_cursor + Reline::IOGate.move_cursor_up 1 + end + Reline::IOGate.erase_after_cursor + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + end + + def render_full_content + lines = @buffer_of_lines.size.times.map do |i| + line = prompt_list[i] + modified_lines[i] + wrapped_lines, = split_by_width(line, screen_width) + wrapped_lines.last.empty? ? "#{line} " : line + end + @output.puts lines.map { |l| "#{l}\r\n" }.join + end + + def print_nomultiline_prompt(prompt) + return unless prompt && !@is_multiline + + # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence. + @rendered_screen.lines = [[[0, Reline::Unicode.calculate_width(prompt, true), prompt]]] + @rendered_screen.cursor_y = 0 + @output.write prompt + end + + def render_differential + wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + + rendered_lines = @rendered_screen.lines + new_lines = wrapped_prompt_and_input_lines.flatten(1)[screen_scroll_top, screen_height].map do |prompt, line| + prompt_width = Reline::Unicode.calculate_width(prompt, true) + [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]] + end + if @menu_info + @menu_info.lines(screen_width).each do |item| + new_lines << [[0, Reline::Unicode.calculate_width(item), item]] 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 + @menu_info = nil # TODO: do not change state here + end + + @dialogs.each_with_index do |dialog, index| + next unless dialog.contents + + x_range, y_range = dialog_range dialog, wrapped_cursor_y - screen_scroll_top + y_range.each do |row| + next if row < 0 || row >= screen_height + dialog_rows = new_lines[row] ||= [] + # index 0 is for prompt, index 1 is for line, index 2.. is for dialog + dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] end end + + cursor_y = @rendered_screen.cursor_y + if new_lines != rendered_lines + # Hide cursor while rendering to avoid cursor flickering. + Reline::IOGate.hide_cursor + num_lines = [[new_lines.size, rendered_lines.size].max, screen_height].min + if @rendered_screen.base_y + num_lines > screen_height + Reline::IOGate.scroll_down(num_lines - cursor_y - 1) + @rendered_screen.base_y = screen_height - num_lines + cursor_y = num_lines - 1 + end + num_lines.times do |i| + rendered_line = rendered_lines[i] || [] + line_to_render = new_lines[i] || [] + next if rendered_line == line_to_render + + Reline::IOGate.move_cursor_down i - cursor_y + cursor_y = i + unless rendered_lines[i] + Reline::IOGate.move_cursor_column 0 + Reline::IOGate.erase_after_cursor + end + render_line_differential(rendered_line, line_to_render) + end + @rendered_screen.lines = new_lines + Reline::IOGate.show_cursor + end + y = wrapped_cursor_y - screen_scroll_top + Reline::IOGate.move_cursor_column wrapped_cursor_x + Reline::IOGate.move_cursor_down y - cursor_y + @rendered_screen.cursor_y = y + new_lines.size - y + end + + def upper_space_height(wrapped_cursor_y) + wrapped_cursor_y - screen_scroll_top + end + + def rest_height(wrapped_cursor_y) + screen_height - wrapped_cursor_y + screen_scroll_top - @rendered_screen.base_y - 1 + end + + def rerender + render_differential unless @in_pasting end class DialogProcScope + CompletionJourneyData = Struct.new(:preposing, :postposing, :list, :pointer) + def initialize(line_editor, config, proc_to_exec, context) @line_editor = line_editor @config = config @@ -559,21 +621,20 @@ class Reline::LineEditor end def screen_width - @line_editor.instance_variable_get(:@screen_size).last + @line_editor.screen_width end def screen_height - @line_editor.instance_variable_get(:@screen_size).first + @line_editor.screen_height end def preferred_dialog_height - rest_height = @line_editor.instance_variable_get(:@rest_height) - scroll_partial_screen = @line_editor.instance_variable_get(:@scroll_partial_screen) || 0 - [cursor_pos.y - scroll_partial_screen, rest_height, (screen_height + 6) / 5].max + _wrapped_cursor_x, wrapped_cursor_y = @line_editor.wrapped_cursor_position + [@line_editor.upper_space_height(wrapped_cursor_y), @line_editor.rest_height(wrapped_cursor_y), (screen_height + 6) / 5].max end def completion_journey_data - @line_editor.instance_variable_get(:@completion_journey_data) + @line_editor.dialog_proc_scope_completion_journey_data end def config @@ -642,27 +703,6 @@ class Reline::LineEditor end DIALOG_DEFAULT_HEIGHT = 20 - private def render_dialog(cursor_column) - changes = @dialogs.map do |dialog| - old_dialog = dialog.dup - update_each_dialog(dialog, cursor_column) - [old_dialog, dialog] - end - render_dialog_changes(changes, cursor_column) - end - - private def padding_space_with_escape_sequences(str, width) - padding_width = width - calculate_width(str, true) - # padding_width should be only positive value. But macOS and Alacritty returns negative value. - padding_width = 0 if padding_width < 0 - str + (' ' * padding_width) - end - - private def range_subtract(base_ranges, subtract_ranges) - indices = base_ranges.flat_map(&:to_a).uniq.sort - subtract_ranges.flat_map(&:to_a) - chunks = indices.chunk_while { |a, b| a + 1 == b } - chunks.map { |a| a.first...a.last + 1 } - end private def dialog_range(dialog, dialog_y) x_range = dialog.column...dialog.column + dialog.width @@ -670,106 +710,9 @@ class Reline::LineEditor [x_range, y_range] end - private def render_dialog_changes(changes, cursor_column) - # Collect x-coordinate range and content of previous and current dialogs for each line - old_dialog_ranges = {} - new_dialog_ranges = {} - new_dialog_contents = {} - changes.each do |old_dialog, new_dialog| - if old_dialog.contents - x_range, y_range = dialog_range(old_dialog, @previous_rendered_dialog_y) - y_range.each do |y| - (old_dialog_ranges[y] ||= []) << x_range - end - end - if new_dialog.contents - x_range, y_range = dialog_range(new_dialog, @first_line_started_from + @started_from) - y_range.each do |y| - (new_dialog_ranges[y] ||= []) << x_range - (new_dialog_contents[y] ||= []) << [x_range, new_dialog.contents[y - y_range.begin]] - end - end - end - return if old_dialog_ranges.empty? && new_dialog_ranges.empty? - - # Calculate x-coordinate ranges to restore text that was hidden behind dialogs for each line - ranges_to_restore = {} - subtract_cache = {} - old_dialog_ranges.each do |y, old_x_ranges| - new_x_ranges = new_dialog_ranges[y] || [] - ranges = subtract_cache[[old_x_ranges, new_x_ranges]] ||= range_subtract(old_x_ranges, new_x_ranges) - ranges_to_restore[y] = ranges if ranges.any? - end - - # Create visual_lines for restoring text hidden behind dialogs - if ranges_to_restore.any? - lines = whole_lines - prompt, _prompt_width, prompt_list = check_multiline_prompt(lines, force_recalc: true) - modified_lines = modify_lines(lines, force_recalc: true) - visual_lines = [] - modified_lines.each_with_index { |l, i| - pr = prompt_list ? prompt_list[i] : prompt - vl, = split_by_width(pr + l, @screen_size.last) - vl.compact! - visual_lines.concat(vl) - } - end - - # Clear and rerender all dialogs line by line - Reline::IOGate.hide_cursor - ymin, ymax = (ranges_to_restore.keys + new_dialog_ranges.keys).minmax - scroll_partial_screen = @scroll_partial_screen || 0 - screen_y_range = scroll_partial_screen..(scroll_partial_screen + @screen_height - 1) - ymin = ymin.clamp(screen_y_range.begin, screen_y_range.end) - ymax = ymax.clamp(screen_y_range.begin, screen_y_range.end) - dialog_y = @first_line_started_from + @started_from - cursor_y = dialog_y - if @highest_in_all <= ymax - scroll_down(ymax - cursor_y) - move_cursor_up(ymax - cursor_y) - end - (ymin..ymax).each do |y| - move_cursor_down(y - cursor_y) - cursor_y = y - new_x_ranges = new_dialog_ranges[y] - restore_ranges = ranges_to_restore[y] - # Restore text that was hidden behind dialogs - if restore_ranges - line = visual_lines[y] || '' - restore_ranges.each do |range| - col = range.begin - width = range.end - range.begin - s = padding_space_with_escape_sequences(Reline::Unicode.take_range(line, col, width), width) - Reline::IOGate.move_cursor_column(col) - @output.write "\e[0m#{s}\e[0m" - end - max_column = [calculate_width(line, true), new_x_ranges&.map(&:end)&.max || 0].max - if max_column < restore_ranges.map(&:end).max - Reline::IOGate.move_cursor_column(max_column) - Reline::IOGate.erase_after_cursor - end - end - # Render dialog contents - new_dialog_contents[y]&.each do |x_range, content| - Reline::IOGate.move_cursor_column(x_range.begin) - @output.write "\e[0m#{content}\e[0m" - end - end - move_cursor_up(cursor_y - dialog_y) - Reline::IOGate.move_cursor_column(cursor_column) - Reline::IOGate.show_cursor - - @previous_rendered_dialog_y = dialog_y - end - - private def update_each_dialog(dialog, cursor_column) - if @in_pasting - dialog.contents = nil - dialog.trap_key = nil - return - end - dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from) - dialog_render_info = dialog.call(@last_key) + private def update_each_dialog(dialog, cursor_column, cursor_row, key = nil) + dialog.set_cursor_pos(cursor_column, cursor_row) + dialog_render_info = dialog.call(key) if dialog_render_info.nil? or dialog_render_info.contents.nil? or dialog_render_info.contents.empty? dialog.contents = nil dialog.trap_key = nil @@ -809,45 +752,41 @@ class Reline::LineEditor else scrollbar_pos = nil end - upper_space = @first_line_started_from - @started_from dialog.column = dialog_render_info.pos.x dialog.width += @block_elem_width if scrollbar_pos - diff = (dialog.column + dialog.width) - (@screen_size.last) + diff = (dialog.column + dialog.width) - screen_width if diff > 0 dialog.column -= diff end - if (@rest_height - dialog_render_info.pos.y) >= height + if rest_height(screen_scroll_top + cursor_row) - dialog_render_info.pos.y >= height dialog.vertical_offset = dialog_render_info.pos.y + 1 - elsif upper_space >= height + elsif cursor_row >= height dialog.vertical_offset = dialog_render_info.pos.y - height else dialog.vertical_offset = dialog_render_info.pos.y + 1 end if dialog.column < 0 dialog.column = 0 - dialog.width = @screen_size.last + dialog.width = screen_width end + face = Reline::Face[dialog_render_info.face || :default] + scrollbar_sgr = face[:scrollbar] + default_sgr = face[:default] + enhanced_sgr = face[:enhanced] dialog.contents = contents.map.with_index do |item, i| - if i == pointer - fg_color = dialog_render_info.pointer_fg_color - bg_color = dialog_render_info.pointer_bg_color - else - fg_color = dialog_render_info.fg_color - bg_color = dialog_render_info.bg_color - end + line_sgr = i == pointer ? enhanced_sgr : default_sgr str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width) - str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width) - colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}" + str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true) + colored_content = "#{line_sgr}#{str}" if scrollbar_pos - color_seq = "\e[37m" if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height) - colored_content + color_seq + @full_block + colored_content + scrollbar_sgr + @full_block elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height) - colored_content + color_seq + @upper_half_block + colored_content + scrollbar_sgr + @upper_half_block elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height) - colored_content + color_seq + @lower_half_block + colored_content + scrollbar_sgr + @lower_half_block else - colored_content + color_seq + ' ' * @block_elem_width + colored_content + scrollbar_sgr + ' ' * @block_elem_width end else colored_content @@ -855,379 +794,20 @@ class Reline::LineEditor end end - private def clear_dialog(cursor_column) - changes = @dialogs.map do |dialog| - old_dialog = dialog.dup - dialog.contents = nil - [old_dialog, dialog] - end - render_dialog_changes(changes, cursor_column) - end - - private def clear_dialog_with_trap_key(cursor_column) - clear_dialog(cursor_column) - @dialogs.each do |dialog| - dialog.trap_key = nil - 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, prompt_list) - @buffer_of_lines[@previous_line_index] = @line - @line = @buffer_of_lines[@line_index] - @previous_line_index = nil - if @in_pasting - scroll_down(1) - else - lines = whole_lines - prev_line_prompt = @prompt_proc ? prompt_list[@line_index - 1] : prompt - prev_line_prompt_width = @prompt_proc ? calculate_width(prev_line_prompt, true) : prompt_width - prev_line = modify_lines(lines)[@line_index - 1] - move_cursor_up(@started_from) - render_partial(prev_line_prompt, prev_line_prompt_width, prev_line, @first_line_started_from + @started_from, with_control: false) - scroll_down(1) - 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 - end - - def just_move_cursor - prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines) - 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 - @cursor, @cursor_max, _, @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 + @cursor) - 1 - calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from) - @previous_line_index = nil - @line = @buffer_of_lines[@line_index] - if @rerender_all - rerender_all_lines - @rerender_all = false - true - else - @first_line_started_from = new_first_line_started_from - @started_from = new_started_from - 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 - new_lines = whole_lines - prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines) - 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) - 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 - 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, force_recalc: false) - return before if !force_recalc && (before.nil? || before.empty? || simplified_rendering?) - - if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?) + private def modify_lines(before, complete) + if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: complete) 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) + before.map { |l| Reline::Unicode.escape_for_print(l) } 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(1) - 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) + private def menu(_target, list) + @menu_info = MenuInfo.new(list) end private def complete_internal_proc(list, is_menu) @@ -1255,7 +835,7 @@ class Reline::LineEditor item_mbchars = item.grapheme_clusters end size = [memo_mbchars.size, item_mbchars.size].min - result = '' + result = +'' size.times do |i| if @config.completion_ignore_case if memo_mbchars[i].casecmp?(item_mbchars[i]) @@ -1276,9 +856,9 @@ class Reline::LineEditor [target, preposing, completed, postposing] end - private def complete(list, just_show_list = false) + private def perform_completion(list, just_show_list) case @completion_state - when CompletionState::NORMAL, CompletionState::JOURNEY + when CompletionState::NORMAL @completion_state = CompletionState::COMPLETION when CompletionState::PERFECT_MATCH @dig_perfect_match_proc&.(@perfect_matched) @@ -1305,100 +885,80 @@ class Reline::LineEditor @completion_state = CompletionState::PERFECT_MATCH else @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH + perform_completion(list, true) if @config.show_all_if_ambiguous end @perfect_matched = completed else @completion_state = CompletionState::MENU + perform_completion(list, true) if @config.show_all_if_ambiguous end if not just_show_list and target < completed - @line = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding) - line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n").last || String.new(encoding: @encoding) - @cursor_max = calculate_width(@line) - @cursor = calculate_width(line_to_pointer) + @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding) + line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n")[@line_index] || String.new(encoding: @encoding) @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) - if @completion_journey_data.list.size == 1 - @completion_journey_data.pointer = 0 - else - case direction - when :up - @completion_journey_data.pointer = @completion_journey_data.list.size - 1 - when :down - @completion_journey_data.pointer = 1 - end - end - @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 + def dialog_proc_scope_completion_journey_data + return nil unless @completion_journey_state + line_index = @completion_journey_state.line_index + pre_lines = @buffer_of_lines[0...line_index].map { |line| line + "\n" } + post_lines = @buffer_of_lines[(line_index + 1)..-1].map { |line| line + "\n" } + DialogProcScope::CompletionJourneyData.new( + pre_lines.join + @completion_journey_state.pre, + @completion_journey_state.post + post_lines.join, + @completion_journey_state.list, + @completion_journey_state.pointer + ) + end + + private def move_completed_list(direction) + @completion_journey_state ||= retrieve_completion_journey_state + return false unless @completion_journey_state + + if (delta = { up: -1, down: +1 }[direction]) + @completion_journey_state.pointer = (@completion_journey_state.pointer + delta) % @completion_journey_state.list.size end - completed = @completion_journey_data.list[@completion_journey_data.pointer] - new_line = (@completion_journey_data.preposing + completed + @completion_journey_data.postposing).split("\n")[@line_index] - @line = new_line.nil? ? String.new(encoding: @encoding) : new_line - line_to_pointer = (@completion_journey_data.preposing + completed).split("\n").last - line_to_pointer = String.new(encoding: @encoding) if line_to_pointer.nil? - @cursor_max = calculate_width(@line) - @cursor = calculate_width(line_to_pointer) - @byte_pointer = line_to_pointer.bytesize + completed = @completion_journey_state.list[@completion_journey_state.pointer] + set_current_line(@completion_journey_state.pre + completed + @completion_journey_state.post, @completion_journey_state.pre.bytesize + completed.bytesize) + true + end + + private def retrieve_completion_journey_state + preposing, target, postposing = retrieve_completion_block + list = call_completion_proc + return unless list.is_a?(Array) + + candidates = list.select{ |item| item.start_with?(target) } + return if candidates.empty? + + pre = preposing.split("\n", -1).last || '' + post = postposing.split("\n", -1).first || '' + CompletionJourneyState.new( + @line_index, pre, target, post, [target] + candidates, 0 + ) end private def run_for_operators(key, method_symbol, &block) - if @waiting_operator_proc + if @vi_waiting_operator 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 + old_byte_pointer = @byte_pointer + @vi_arg = (@vi_arg || 1) * @vi_waiting_operator_arg 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 - } + byte_pointer_diff = @byte_pointer - old_byte_pointer + @byte_pointer = old_byte_pointer + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) + cleanup_waiting end else # Ignores operator when not motion is given. block.(false) + cleanup_waiting end - @waiting_operator_proc = nil - @waiting_operator_vi_arg = nil - if @vi_arg - @rerender_all = true - @vi_arg = nil - end + @vi_arg = nil else block.(false) end @@ -1415,7 +975,7 @@ class Reline::LineEditor 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? + if @config.editing_mode_is?(:emacs, :vi_insert) and @vi_waiting_operator.nil? not_insertion = method_symbol != :ed_insert process_insert(force: not_insertion) end @@ -1434,11 +994,33 @@ class Reline::LineEditor end end + private def cleanup_waiting + @waiting_proc = nil + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + @searching_prompt = nil + @drop_terminate_spaces = false + end + private def process_key(key, method_symbol) + if key.is_a?(Symbol) + cleanup_waiting + elsif @waiting_proc + old_byte_pointer = @byte_pointer + @waiting_proc.call(key) + if @vi_waiting_operator + byte_pointer_diff = @byte_pointer - old_byte_pointer + @byte_pointer = old_byte_pointer + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) + cleanup_waiting + end + @kill_ring.process + return + end + 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) @@ -1450,7 +1032,6 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_al = true @vi_arg = nil end elsif @vi_arg @@ -1461,8 +1042,6 @@ class Reline::LineEditor 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 @@ -1470,13 +1049,9 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_all = true @vi_arg = nil end 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) @@ -1492,11 +1067,6 @@ class Reline::LineEditor 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? @@ -1510,97 +1080,107 @@ class Reline::LineEditor 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) + if @config.editing_mode_is?(:vi_command, :vi_insert) + # split ESC + key in vi mode + 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) + end 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) + if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize + byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @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 update(key) + modified = input_key(key) + unless @in_pasting + scroll_into_view + @just_cursor_moving = !modified + update_dialogs(key) + @just_cursor_moving = false end end def input_key(key) - @last_key = key + save_old_buffer @config.reset_oneshot_key_bindings @dialogs.each do |dialog| if key.char.instance_of?(Symbol) and key.char == dialog.name return end end - @just_cursor_moving = nil if key.char.nil? + process_insert(force: true) if @first_char - @line = nil + @eof = true 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 - if @config.autocompletion - move_completed_list(result, :down) - else - complete(result) - end - end - end - elsif @config.editing_mode_is?(:emacs, :vi_insert) and key.char == :completion_journey_up - if not @config.disable_completion and @config.autocompletion - result = call_completion_proc - if result.is_a?(Array) - completion_occurs = true - process_insert - move_completed_list(result, :up) - 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) + @completion_occurs = false + + if key.char.is_a?(Symbol) process_key(key.char, key.char) else normal_char(key) end - unless completion_occurs + unless @completion_occurs @completion_state = CompletionState::NORMAL - @completion_journey_data = nil + @completion_journey_state = nil 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 + + push_past_lines unless @undoing + @undoing = false + + if @in_pasting + clear_dialogs + return + end + + modified = @old_buffer_of_lines != @buffer_of_lines + if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion + # Auto complete starts only when edited + process_insert(force: true) + @completion_journey_state = retrieve_completion_journey_state + end + modified + end + + def save_old_buffer + @old_buffer_of_lines = @buffer_of_lines.dup + @old_byte_pointer = @byte_pointer.dup + @old_line_index = @line_index.dup + end + + def push_past_lines + if @old_buffer_of_lines != @buffer_of_lines + @past_lines.push([@old_buffer_of_lines, @old_byte_pointer, @old_line_index]) end - if @is_multiline and @auto_indent_proc and not simplified_rendering? - process_auto_indent + trim_past_lines + end + + MAX_PAST_LINES = 100 + def trim_past_lines + if @past_lines.size > MAX_PAST_LINES + @past_lines.shift + end + end + + def scroll_into_view + _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + if wrapped_cursor_y < screen_scroll_top + @scroll_partial_screen = wrapped_cursor_y + end + if wrapped_cursor_y >= screen_scroll_top + screen_height + @scroll_partial_screen = wrapped_cursor_y - screen_height + 1 end end @@ -1634,43 +1214,52 @@ class Reline::LineEditor 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 - 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 - new_lines = whole_lines - new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent) - if new_indent&.>= 0 - md = new_lines[@line_index].match(/\A */) - prev_indent = md[0].count(' ') - if @check_new_auto_indent - line = @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip - @cursor = new_indent - @cursor_max = calculate_width(line) - @byte_pointer = new_indent - else - @line = ' ' * new_indent + @line.lstrip - @cursor += new_indent - prev_indent - @cursor_max = calculate_width(@line) - @byte_pointer += new_indent - prev_indent - end + private def process_auto_indent(line_index = @line_index, cursor_dependent: true, add_newline: false) + return if @in_pasting + return unless @auto_indent_proc + + line = @buffer_of_lines[line_index] + byte_pointer = cursor_dependent && @line_index == line_index ? @byte_pointer : line.bytesize + new_indent = @auto_indent_proc.(@buffer_of_lines.take(line_index + 1).push(''), line_index, byte_pointer, add_newline) + return unless new_indent + + new_line = ' ' * new_indent + line.lstrip + @buffer_of_lines[line_index] = new_line + if @line_index == line_index + indent_diff = new_line.bytesize - line.bytesize + @byte_pointer = [@byte_pointer + indent_diff, 0].max end - @check_new_auto_indent = false + end + + def line() + @buffer_of_lines.join("\n") unless eof? + end + + def current_line + @buffer_of_lines[@line_index] + end + + def set_current_line(line, byte_pointer = nil) + cursor = current_byte_pointer_cursor + @buffer_of_lines[@line_index] = line + if byte_pointer + @byte_pointer = byte_pointer + else + calculate_nearest_cursor(cursor) + end + process_auto_indent + end + + def set_current_lines(lines, byte_pointer = nil, line_index = 0) + cursor = current_byte_pointer_cursor + @buffer_of_lines = lines + @line_index = line_index + if byte_pointer + @byte_pointer = byte_pointer + else + calculate_nearest_cursor(cursor) + end + process_auto_indent end def retrieve_completion_block(set_completion_quote_character = false) @@ -1684,7 +1273,7 @@ class Reline::LineEditor else quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ end - before = @line.byteslice(0, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil quote = nil @@ -1692,7 +1281,7 @@ class Reline::LineEditor escaped_quote = nil i = 0 while i < @byte_pointer do - slice = @line.byteslice(i, @byte_pointer - i) + slice = current_line.byteslice(i, @byte_pointer - i) unless slice.valid_encoding? i += 1 next @@ -1714,15 +1303,15 @@ class Reline::LineEditor elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 - before = @line.byteslice(i, @byte_pointer - i) + before = current_line.byteslice(i, @byte_pointer - i) break_pointer = i else i += 1 end end - postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) + postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) if rest - preposing = @line.byteslice(0, break_pointer) + preposing = current_line.byteslice(0, break_pointer) target = rest if set_completion_quote_character and quote Reline.core.instance_variable_set(:@completion_quote_character, quote) @@ -1733,126 +1322,93 @@ class Reline::LineEditor else preposing = '' if break_pointer - preposing = @line.byteslice(0, break_pointer) + preposing = current_line.byteslice(0, break_pointer) else preposing = '' end target = before end - if @is_multiline - lines = whole_lines - 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 + lines = whole_lines + 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 [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_pasted_text(text) + save_old_buffer + pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer) + post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..) + lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1) + lines << '' if lines.empty? + @buffer_of_lines[@line_index, 1] = lines + @line_index += lines.size - 1 + @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize + push_past_lines + end + def insert_text(text) - width = calculate_width(text) - if @cursor == @cursor_max - @line += text + if @buffer_of_lines[@line_index].bytesize == @byte_pointer + @buffer_of_lines[@line_index] += text else - @line = byteinsert(@line, @byte_pointer, text) + @buffer_of_lines[@line_index] = byteinsert(@buffer_of_lines[@line_index], @byte_pointer, text) end @byte_pointer += text.bytesize - @cursor += width - @cursor_max += width + process_auto_indent 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 + if @buffer_of_lines.size == 1 + @buffer_of_lines[@line_index] = '' + @byte_pointer = 0 + elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0 + @buffer_of_lines.pop + @line_index -= 1 + @byte_pointer = 0 + elsif @line_index < (@buffer_of_lines.size - 1) + @buffer_of_lines.delete_at(@line_index) @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) + if current_line + before = current_line.byteslice(0, start) + after = current_line.byteslice(start + length, current_line.bytesize) + set_current_line(before + after) 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 + last = current_line.bytesize - 1 if last > current_line.bytesize + last += current_line.bytesize if last < 0 + first += current_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) + line = current_line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding) + set_current_line(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) + set_current_line(current_line.byteslice(0, start)) 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 = @previous_line_index || @line_index - temp_lines = @buffer_of_lines.dup - temp_lines[index] = @line - temp_lines + @buffer_of_lines.dup end def whole_buffer - if @buffer_of_lines.size == 1 and @line.nil? - nil - else - whole_lines.join("\n") - end + whole_lines.join("\n") end def finished? @@ -1861,7 +1417,6 @@ class Reline::LineEditor def finish @finished = true - @rerender_all = true @config.reset end @@ -1892,33 +1447,56 @@ class Reline::LineEditor 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) + next_line = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) + cursor_line = current_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 complete(_key) + return if @config.disable_completion + + process_insert(force: true) + if @config.autocompletion + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list(:down) + else + @completion_journey_state = nil + result = call_completion_proc + if result.is_a?(Array) + @completion_occurs = true + perform_completion(result, false) + end + end + end + + private def completion_journey_move(direction) + return if @config.disable_completion + + process_insert(force: true) + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list(direction) + end + + private def menu_complete(_key) + completion_journey_move(:down) + end + + private def menu_complete_backward(_key) + completion_journey_move(:up) + end + + private def completion_journey_up(_key) + completion_journey_move(:up) if @config.autocompletion + end + # Editline:: +ed-unassigned+ This editor command always results in an error. # GNU Readline:: There is no corresponding macro. 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 + insert_text(@continuous_insertion_buffer) @continuous_insertion_buffer.clear end @@ -1936,9 +1514,6 @@ class Reline::LineEditor # million. # GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself. private def ed_insert(key) - str = nil - width = nil - bytesize = nil if key.instance_of?(String) begin key.encode(Encoding::UTF_8) @@ -1946,7 +1521,6 @@ class Reline::LineEditor return end str = key - bytesize = key.bytesize else begin key.chr.encode(Encoding::UTF_8) @@ -1954,7 +1528,6 @@ class Reline::LineEditor return end str = key.chr - bytesize = 1 end if @in_pasting @continuous_insertion_buffer << str @@ -1962,28 +1535,8 @@ class Reline::LineEditor 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) - combined_char = last_mbchar + str - if last_byte_size != 0 and combined_char.grapheme_clusters.size == 1 - # combined char - last_mbchar_width = Reline::Unicode.get_mbchar_width(last_mbchar) - combined_char_width = Reline::Unicode.get_mbchar_width(combined_char) - if combined_char_width > last_mbchar_width - width = combined_char_width - last_mbchar_width - else - width = 0 - end - end - @cursor += width - @cursor_max += width + + insert_text(str) end alias_method :ed_digit, :ed_insert alias_method :self_insert, :ed_insert @@ -2005,18 +1558,11 @@ class Reline::LineEditor 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_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + if (@byte_pointer < current_line.bytesize) @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 + elsif @config.editing_mode_is?(:emacs) and @byte_pointer == current_line.bytesize and @line_index < @buffer_of_lines.size - 1 @byte_pointer = 0 - @cursor_max = calculate_width(next_line) - @previous_line_index = @line_index @line_index += 1 end arg -= 1 @@ -2025,19 +1571,12 @@ class Reline::LineEditor 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) + if @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_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 + elsif @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0 @line_index -= 1 + @byte_pointer = current_line.bytesize end arg -= 1 ed_prev_char(key, arg: arg) if arg > 0 @@ -2045,157 +1584,109 @@ class Reline::LineEditor alias_method :backward_char, :ed_prev_char private def vi_first_print(key) - @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line) + @byte_pointer, = Reline::Unicode.vi_first_print(current_line) end private def ed_move_to_beg(key) - @byte_pointer = @cursor = 0 + @byte_pointer = 0 end alias_method :beginning_of_line, :ed_move_to_beg + alias_method :vi_zero, :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 + @byte_pointer = current_line.bytesize 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' + private def generate_searcher(search_key) + search_word = String.new(encoding: @encoding) + multibyte_buf = String.new(encoding: 'ASCII-8BIT') + hit_pointer = nil + lambda do |key| + search_again = false + case key + 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 search_key == key + 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 - 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 + hit = nil + if not search_word.empty? and @line_backup_in_history&.include?(search_word) + hit_pointer = Reline::HISTORY.size + 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 - 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 + if @history_pointer + case search_key when "\C-r".ord history_pointer_base = 0 - history = Reline::HISTORY[0..@history_pointer] + history = Reline::HISTORY[0..(@history_pointer - 1)] when "\C-s".ord - history_pointer_base = @history_pointer - history = Reline::HISTORY[@history_pointer..-1] + history_pointer_base = @history_pointer + 1 + history = Reline::HISTORY[(@history_pointer + 1)..-1] end else history_pointer_base = 0 history = Reline::HISTORY end - case prev_search_key + elsif @history_pointer + case search_key when "\C-r".ord - hit_index = history.rindex { |item| - item.include?(search_word) - } + history_pointer_base = 0 + history = Reline::HISTORY[0..@history_pointer] 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] + 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 + case search_key when "\C-r".ord - prompt_name = 'reverse-i-search' + hit_index = history.rindex { |item| + item.include?(search_word) + } when "\C-s".ord - prompt_name = 'i-search' + hit_index = history.index { |item| + item.include?(search_word) + } 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 - @byte_pointer = @line.bytesize - @cursor = @cursor_max = calculate_width(@line) - @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 + if hit_index + hit_pointer = history_pointer_base + hit_index + hit = Reline::HISTORY[hit_pointer] end end + case search_key + when "\C-r".ord + prompt_name = 'reverse-i-search' + when "\C-s".ord + prompt_name = 'i-search' + end + prompt_name = "failed #{prompt_name}" unless hit + [search_word, prompt_name, hit_pointer] 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 + @line_backup_in_history = whole_buffer end - searcher = generate_searcher - searcher.resume(key) + searcher = generate_searcher(key) @searching_prompt = "(reverse-i-search)`': " termination_keys = ["\C-j".ord] termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators @@ -2207,67 +1698,41 @@ class Reline::LineEditor 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 + @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 @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) + @byte_pointer = 0 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 + @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 + move_history(nil, line: :end, cursor: :end, save_buffer: false) @searching_prompt = nil @waiting_proc = nil - @line_backup_in_history = nil - @cursor_max = calculate_width(@line) - @cursor = @byte_pointer = 0 - @rerender_all = true + @byte_pointer = 0 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) + search_word, prompt_name, hit_pointer = searcher.call(k) + Reline.last_incremental_search = search_word + @searching_prompt = "(%s)`%s'" % [prompt_name, search_word] + @searching_prompt += ': ' unless @is_multiline + move_history(hit_pointer, line: :end, cursor: :end, save_buffer: false) if hit_pointer 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 + @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 @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) + @byte_pointer = 0 end end } @@ -2283,199 +1748,95 @@ class Reline::LineEditor 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] + private def search_history(prefix, pointer_range) + pointer_range.each do |pointer| + lines = Reline::HISTORY[pointer].split("\n") + lines.each_with_index do |line, index| + return [pointer, index] if line.start_with?(prefix) + end end - @cursor_max = calculate_width(@line) + nil + end + + private def ed_search_prev_history(key, arg: 1) + substr = current_line.byteslice(0, @byte_pointer) + return if @history_pointer == 0 + return if @history_pointer.nil? && substr.empty? && !current_line.empty? + + history_range = 0...(@history_pointer || Reline::HISTORY.size) + h_pointer, line_index = search_history(substr, history_range.reverse_each) + return unless h_pointer + move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) 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 + substr = current_line.byteslice(0, @byte_pointer) + return if @history_pointer.nil? + + history_range = @history_pointer + 1...Reline::HISTORY.size + h_pointer, line_index = search_history(substr, history_range) 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) + + move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) 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 + private def move_history(history_pointer, line:, cursor:, save_buffer: true) + history_pointer ||= Reline::HISTORY.size + return if history_pointer < 0 || history_pointer > Reline::HISTORY.size + old_history_pointer = @history_pointer || Reline::HISTORY.size + if old_history_pointer == Reline::HISTORY.size + @line_backup_in_history = save_buffer ? whole_buffer : '' + else + Reline::HISTORY[old_history_pointer] = whole_buffer if save_buffer 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 + if history_pointer == Reline::HISTORY.size + buf = @line_backup_in_history + @history_pointer = @line_backup_in_history = nil 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 + buf = Reline::HISTORY[history_pointer] + @history_pointer = history_pointer 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) + @buffer_of_lines = buf.split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = line == :start ? 0 : line == :end ? @buffer_of_lines.size - 1 : line + @byte_pointer = cursor == :start ? 0 : cursor == :end ? current_line.bytesize : cursor + end + + private def ed_prev_history(key, arg: 1) + if @line_index > 0 + cursor = current_byte_pointer_cursor + @line_index -= 1 + calculate_nearest_cursor(cursor) + return end + move_history( + (@history_pointer || Reline::HISTORY.size) - 1, + line: :end, + cursor: @config.editing_mode_is?(:vi_command) ? :start : :end, + ) arg -= 1 ed_prev_history(key, arg: arg) if arg > 0 end alias_method :previous_history, :ed_prev_history private def ed_next_history(key, arg: 1) - if @is_multiline and @line_index < (@buffer_of_lines.size - 1) - @previous_line_index = @line_index + if @line_index < (@buffer_of_lines.size - 1) + cursor = current_byte_pointer_cursor @line_index += 1 + calculate_nearest_cursor(cursor) 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 + move_history( + (@history_pointer || Reline::HISTORY.size) + 1, + line: :start, + cursor: @config.editing_mode_is?(:vi_command) ? :start : :end, + ) arg -= 1 ed_next_history(key, arg: arg) if arg > 0 end @@ -2500,40 +1861,29 @@ class Reline::LineEditor end else # should check confirm_multiline_termination to finish? - @previous_line_index = @line_index @line_index = @buffer_of_lines.size - 1 + @byte_pointer = current_line.bytesize 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, arg: 1) - 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 + arg.times do + if @byte_pointer == 0 and @line_index > 0 + @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 + elsif @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) + line, = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) + end end - arg -= 1 - em_delete_prev_char(key, arg: arg) if arg > 0 + process_auto_indent end alias_method :backward_delete_char, :em_delete_prev_char @@ -2543,23 +1893,23 @@ class Reline::LineEditor # the line. With a negative numeric argument, kill backward # from the cursor to the beginning of the current line. 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) + if current_line.bytesize > @byte_pointer + line, deleted = byteslice!(current_line, @byte_pointer, current_line.bytesize - @byte_pointer) + set_current_line(line, line.bytesize) @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 + elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1 + set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize) end end alias_method :kill_line, :ed_kill_line + # Editline:: +vi_change_to_eol+ (vi command: +C+) + Kill and change from the cursor to the end of the line. + private def vi_change_to_eol(key) + ed_kill_line(key) + + @config.editing_mode = :vi_insert + end + # Editline:: +vi-kill-line-prev+ (vi: +Ctrl-U+) Delete the string from the # beginning of the edit buffer to the cursor and save it to the # cut buffer. @@ -2567,11 +1917,9 @@ class Reline::LineEditor # to the beginning of the current line. private def vi_kill_line_prev(key) if @byte_pointer > 0 - @line, deleted = byteslice!(@line, 0, @byte_pointer) - @byte_pointer = 0 + line, deleted = byteslice!(current_line, 0, @byte_pointer) + set_current_line(line, 0) @kill_ring.append(deleted, true) - @cursor_max = calculate_width(@line) - @cursor = 0 end end alias_method :unix_line_discard, :vi_kill_line_prev @@ -2581,50 +1929,35 @@ class Reline::LineEditor # GNU Readline:: +kill-whole-line+ (not bound) Kill all characters on the # current line, no matter where point is. private def em_kill_line(key) - if @line.size > 0 - @kill_ring.append(@line.dup, true) - @line.clear - @byte_pointer = 0 - @cursor_max = 0 - @cursor = 0 + if current_line.size > 0 + @kill_ring.append(current_line.dup, true) + set_current_line('', 0) end end alias_method :kill_whole_line, :em_kill_line private def em_delete(key) - if @line.empty? and (not @is_multiline or @buffer_of_lines.size == 1) and key == "\C-d".ord - @line = nil - if @buffer_of_lines.size > 1 - scroll_down(@highest_in_all - @first_line_started_from) - end - Reline::IOGate.move_cursor_column(0) + if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord @eof = true finish - elsif @byte_pointer < @line.bytesize - splitted_last = @line.byteslice(@byte_pointer, @line.bytesize) + elsif @byte_pointer < current_line.bytesize + splitted_last = current_line.byteslice(@byte_pointer, current_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 + line, = byteslice!(current_line, @byte_pointer, mbchar.bytesize) + set_current_line(line) + elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1 + set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize) end end alias_method :delete_char, :em_delete private def em_delete_or_list(key) - if @line.empty? or @byte_pointer < @line.bytesize + if current_line.empty? or @byte_pointer < current_line.bytesize em_delete(key) - else # show completed list + elsif !@config.autocompletion # show completed list result = call_completion_proc if result.is_a?(Array) - complete(result, true) + perform_completion(result, true) end end end @@ -2632,162 +1965,136 @@ class Reline::LineEditor 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 + insert_text(yanked) if yanked 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 + line, = byteslice!(current_line, @byte_pointer - prev_yank.bytesize, prev_yank.bytesize) + set_current_line(line, @byte_pointer - prev_yank.bytesize) + insert_text(yanked) end end alias_method :yank_pop, :em_yank_pop private def ed_clear_screen(key) - @cleared = true + Reline::IOGate.clear_screen + @screen_size = Reline::IOGate.get_screen_size + @rendered_screen.lines = [] + @rendered_screen.base_y = 0 + @rendered_screen.cursor_y = 0 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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.em_forward_word(current_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_size, _ = Reline::Unicode.em_backward_word(current_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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + line, word = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) @kill_ring.append(word) - @cursor_max -= width end end + alias_method :kill_word, :em_delete_next_word 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) + byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer) + line, word = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) @kill_ring.append(word, true) - @byte_pointer -= byte_size - @cursor -= width - @cursor_max -= width end end + alias_method :backward_kill_word, :ed_delete_prev_word 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 + if @byte_pointer < current_line.bytesize + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) @byte_pointer += byte_size end - back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + back1_byte_size = Reline::Unicode.get_prev_mbchar_size(current_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_byte_size = Reline::Unicode.get_prev_mbchar_size(current_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) + line, back2_mbchar = byteslice!(current_line, back2_pointer, back2_byte_size) + set_current_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) + left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(current_line, @byte_pointer) + before = current_line.byteslice(0, left_word_start) + left_word = current_line.byteslice(left_word_start, middle_start - left_word_start) + middle = current_line.byteslice(middle_start, right_word_start - middle_start) + right_word = current_line.byteslice(right_word_start, after_start - right_word_start) + after = current_line.byteslice(after_start, current_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) + set_current_line(from_head_to_left_word + after, from_head_to_left_word.bytesize) 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) + if current_line.bytesize > @byte_pointer + byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(current_line, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) + after = current_line.byteslice((@byte_pointer + byte_size)..-1) + set_current_line(before + new_str + after, @byte_pointer + new_str.bytesize) 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| + if current_line.bytesize > @byte_pointer + byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + part = current_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 + rest = current_line.byteslice((@byte_pointer + byte_size)..-1) + line = current_line.byteslice(0, @byte_pointer) + part + set_current_line(line + rest, line.bytesize) 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| + if current_line.bytesize > @byte_pointer + byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + part = current_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 + rest = current_line.byteslice((@byte_pointer + byte_size)..-1) + line = current_line.byteslice(0, @byte_pointer) + part + set_current_line(line + rest, line.bytesize) 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 + byte_size, _ = Reline::Unicode.em_big_backward_word(current_line, @byte_pointer) + line, deleted = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) @kill_ring.append(deleted, true) end end @@ -2815,10 +2122,9 @@ class Reline::LineEditor 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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces) @byte_pointer += byte_size - @cursor += width end arg -= 1 vi_next_word(key, arg: arg) if arg > 0 @@ -2826,38 +2132,32 @@ class Reline::LineEditor private def vi_prev_word(key, arg: 1) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer) + byte_size, _ = Reline::Unicode.vi_backward_word(current_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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_forward_end_word(current_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) + byte_size = Reline::Unicode.get_next_mbchar_size(current_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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer) @byte_pointer += byte_size - @cursor += width end arg -= 1 vi_next_big_word(key, arg: arg) if arg > 0 @@ -2865,50 +2165,39 @@ class Reline::LineEditor 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_size, _ = Reline::Unicode.vi_big_backward_word(current_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) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_big_forward_end_word(current_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) + byte_size = Reline::Unicode.get_next_mbchar_size(current_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]) + if @byte_pointer == 0 and @line_index > 0 @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) + process_auto_indent cursor_dependent: false + elsif @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_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 + line, _ = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) end end @@ -2923,78 +2212,81 @@ class Reline::LineEditor end private def ed_delete_prev_char(key, arg: 1) - deleted = '' + deleted = +'' arg.times do - if @cursor > 0 - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + if @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) @byte_pointer -= byte_size - @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) + line, mbchar = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) 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 + private def vi_change_meta(key, arg: nil) + if @vi_waiting_operator + set_current_line('', 0) if @vi_waiting_operator == :vi_change_meta_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @drop_terminate_spaces = true + @vi_waiting_operator = :vi_change_meta_confirm + @vi_waiting_operator_arg = arg || 1 + end 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 + private def vi_change_meta_confirm(byte_pointer_diff) + vi_delete_meta_confirm(byte_pointer_diff) + @config.editing_mode = :vi_insert + @drop_terminate_spaces = false 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 + private def vi_delete_meta(key, arg: nil) + if @vi_waiting_operator + set_current_line('', 0) if @vi_waiting_operator == :vi_delete_meta_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @vi_waiting_operator = :vi_delete_meta_confirm + @vi_waiting_operator_arg = arg || 1 + end + end + + private def vi_delete_meta_confirm(byte_pointer_diff) + if byte_pointer_diff > 0 + line, cut = byteslice!(current_line, @byte_pointer, byte_pointer_diff) + elsif byte_pointer_diff < 0 + line, cut = byteslice!(current_line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) + end + copy_for_vi(cut) + set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) + end + + private def vi_yank(key, arg: nil) + if @vi_waiting_operator + copy_for_vi(current_line) if @vi_waiting_operator == :vi_yank_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @vi_waiting_operator = :vi_yank_confirm + @vi_waiting_operator_arg = arg || 1 + end + end + + private def vi_yank_confirm(byte_pointer_diff) + if byte_pointer_diff > 0 + cut = current_line.byteslice(@byte_pointer, byte_pointer_diff) + elsif byte_pointer_diff < 0 + cut = current_line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) + end + copy_for_vi(cut) 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) + if current_line.empty? and @buffer_of_lines.size == 1 + set_current_line('', 0) @eof = true finish else @@ -3005,18 +2297,15 @@ class Reline::LineEditor 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) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + unless current_line.empty? || byte_size == 0 + line, mbchar = byteslice!(current_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 + if @byte_pointer > 0 && current_line.bytesize == @byte_pointer + byte_size + byte_size = Reline::Unicode.get_prev_mbchar_size(line, @byte_pointer) + set_current_line(line, @byte_pointer - byte_size) + else + set_current_line(line, @byte_pointer) end end arg -= 1 @@ -3027,54 +2316,25 @@ class Reline::LineEditor 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 + move_history(0, line: :start, cursor: :start) 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.write whole_lines.join("\n") 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 + @buffer_of_lines = File.read(path).split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = 0 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 + set_current_line(byteinsert(current_line, @byte_pointer, @vi_clipboard), @byte_pointer + cursor_point.bytesize) end arg -= 1 vi_paste_prev(key, arg: arg) if arg > 0 @@ -3082,11 +2342,9 @@ class Reline::LineEditor 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 + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + line = byteinsert(current_line, @byte_pointer + byte_size, @vi_clipboard) + set_current_line(line, @byte_pointer + @vi_clipboard.bytesize) end arg -= 1 vi_paste_next(key, arg: arg) if arg > 0 @@ -3110,43 +2368,33 @@ class Reline::LineEditor 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] + # Implementing behavior of vi, not Readline's vi-mode. + @byte_pointer, = current_line.grapheme_clusters.inject([0, 0]) { |(total_byte_size, total_width), gc| 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 + break [total_byte_size, total_width] if (total_width + mbchar_width) >= arg + [total_byte_size + gc.bytesize, total_width + mbchar_width] } 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) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + before = current_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) + after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point) + set_current_line(before + k.chr + after) @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) + byte_size += Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer + byte_size) end - before = @line.byteslice(0, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) remaining_point = @byte_pointer + byte_size - after = @line.byteslice(remaining_point, @line.bytesize - remaining_point) + after = current_line.byteslice(remaining_point, current_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) + set_current_line(before + replaced + after, @byte_pointer + replaced.bytesize) @waiting_proc = nil end } @@ -3169,7 +2417,7 @@ class Reline::LineEditor prev_total = nil total = nil found = false - @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| + current_line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point @@ -3189,21 +2437,16 @@ class Reline::LineEditor end end if not need_prev_char and found and total - byte_size, width = total + byte_size, _ = total @byte_pointer += byte_size - @cursor += width elsif need_prev_char and found and prev_total - byte_size, width = prev_total + byte_size, _ = prev_total @byte_pointer += byte_size - @cursor += width end if inclusive - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) + byte_size = Reline::Unicode.get_next_mbchar_size(current_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 @@ -3226,7 +2469,7 @@ class Reline::LineEditor prev_total = nil total = nil found = false - @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| + current_line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point @@ -3246,26 +2489,19 @@ class Reline::LineEditor end end if not need_next_char and found and total - byte_size, width = total + byte_size, _ = total @byte_pointer -= byte_size - @cursor -= width elsif need_next_char and found and prev_total - byte_size, width = prev_total + byte_size, _ = 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 + if @buffer_of_lines.size > @line_index + 1 + next_line = @buffer_of_lines.delete_at(@line_index + 1).lstrip + set_current_line(current_line + ' ' + next_line, current_line.bytesize) end arg -= 1 vi_join_lines(key, arg: arg) if arg > 0 @@ -3279,11 +2515,27 @@ class Reline::LineEditor 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 + + private def emacs_editing_mode(key) + @config.editing_mode = :emacs + end + + private def vi_editing_mode(key) + @config.editing_mode = :vi_insert + end + + private def undo(_key) + return if @past_lines.empty? + + @undoing = true + + target_lines, target_cursor_x, target_cursor_y = @past_lines.last + set_current_lines(target_lines, target_cursor_x, target_cursor_y) + + @past_lines.pop + end end |