diff options
Diffstat (limited to 'lib/reline/line_editor.rb')
-rw-r--r-- | lib/reline/line_editor.rb | 328 |
1 files changed, 199 insertions, 129 deletions
diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 81413505d7..c71a5f79ee 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -4,7 +4,6 @@ require 'reline/unicode' require 'tempfile' class Reline::LineEditor - # TODO: undo # TODO: Use "private alias_method" idiom after drop Ruby 2.5. attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc @@ -46,6 +45,7 @@ class Reline::LineEditor RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true) CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer) + NullActionState = [nil, nil].freeze class MenuInfo attr_reader :list @@ -75,7 +75,7 @@ class Reline::LineEditor def initialize(config, encoding) @config = config @completion_append_character = '' - @screen_size = Reline::IOGate.get_screen_size + @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset reset_variables(encoding: encoding) end @@ -176,9 +176,8 @@ class Reline::LineEditor 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 + clear_rendered_screen_cache + render end private def handle_interrupted @@ -186,11 +185,11 @@ class Reline::LineEditor @interrupted = false clear_dialogs - scrolldown = render_differential - Reline::IOGate.scroll_down scrolldown + render + cursor_to_bottom_offset = @rendered_screen.lines.size - @rendered_screen.cursor_y + Reline::IOGate.scroll_down cursor_to_bottom_offset Reline::IOGate.move_cursor_column 0 - @rendered_screen.lines = [] - @rendered_screen.cursor_y = 0 + clear_rendered_screen_cache case @old_trap when 'DEFAULT', 'SYSTEM_DEFAULT' raise Interrupt @@ -235,11 +234,9 @@ class Reline::LineEditor @vi_waiting_operator_arg = nil @completion_journey_state = nil @completion_state = CompletionState::NORMAL - @completion_occurs = false @perfect_matched = nil @menu_info = nil @searching_prompt = nil - @first_char = true @just_cursor_moving = false @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) @@ -252,6 +249,11 @@ class Reline::LineEditor @resized = false @cache = {} @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0) + @input_lines = [[[""], 0, 0]] + @input_lines_position = 0 + @undoing = false + @prev_action_state = NullActionState + @next_action_state = NullActionState reset_line end @@ -284,7 +286,7 @@ class Reline::LineEditor 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 */, '') + @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 @@ -387,7 +389,7 @@ class Reline::LineEditor 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)).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 @@ -411,12 +413,17 @@ class Reline::LineEditor # do nothing elsif level == :blank Reline::IOGate.move_cursor_column base_x - @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" + @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}" else x, w, content = new_items[level] - content = Reline::Unicode.take_range(content, base_x - x, width) unless x == base_x && w == width - Reline::IOGate.move_cursor_column base_x - @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}" + 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_sequence}#{content}#{Reline::IOGate.reset_color_sequence}" end base_x += width end @@ -452,28 +459,7 @@ class Reline::LineEditor 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 + render_differential([], 0, 0) lines = @buffer_of_lines.size.times.map do |i| line = prompt_list[i] + modified_lines[i] wrapped_lines, = split_by_width(line, screen_width) @@ -482,19 +468,13 @@ class Reline::LineEditor @output.puts lines.map { |l| "#{l}\r\n" }.join end - def print_nomultiline_prompt(prompt) - return unless prompt && !@is_multiline - + def print_nomultiline_prompt # 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 + @output.write @prompt if @prompt && !@is_multiline end - def render_differential + def render 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]] @@ -512,12 +492,21 @@ class Reline::LineEditor 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 + render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top + end + + # Reflects lines to be rendered and new cursor position to the screen + # by calculating the difference from the previous render. + + private def render_differential(new_lines, new_cursor_x, new_cursor_y) + rendered_lines = @rendered_screen.lines cursor_y = @rendered_screen.cursor_y if new_lines != rendered_lines # Hide cursor while rendering to avoid cursor flickering. @@ -544,11 +533,14 @@ class Reline::LineEditor @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 + Reline::IOGate.move_cursor_column new_cursor_x + Reline::IOGate.move_cursor_down new_cursor_y - cursor_y + @rendered_screen.cursor_y = new_cursor_y + end + + private def clear_rendered_screen_cache + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 end def upper_space_height(wrapped_cursor_y) @@ -560,7 +552,7 @@ class Reline::LineEditor end def rerender - render_differential unless @in_pasting + render unless @in_pasting end class DialogProcScope @@ -678,10 +670,8 @@ class Reline::LineEditor @trap_key.each do |t| @config.add_oneshot_key_binding(t, @name) end - elsif @trap_key.is_a?(Array) + else @config.add_oneshot_key_binding(@trap_key, @name) - elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key) - @config.add_oneshot_key_binding([@trap_key], @name) end end dialog_render_info @@ -699,13 +689,6 @@ class Reline::LineEditor DIALOG_DEFAULT_HEIGHT = 20 - 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 dialog_range(dialog, dialog_y) x_range = dialog.column...dialog.column + dialog.width y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size @@ -778,7 +761,7 @@ class Reline::LineEditor dialog.contents = contents.map.with_index do |item, i| 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) + str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true) colored_content = "#{line_sgr}#{str}" if scrollbar_pos if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height) @@ -858,7 +841,7 @@ class Reline::LineEditor [target, preposing, completed, postposing] end - private def complete(list, just_show_list) + private def perform_completion(list, just_show_list) case @completion_state when CompletionState::NORMAL @completion_state = CompletionState::COMPLETION @@ -887,12 +870,12 @@ class Reline::LineEditor @completion_state = CompletionState::PERFECT_MATCH else @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH - complete(list, true) if @config.show_all_if_ambiguous + perform_completion(list, true) if @config.show_all_if_ambiguous end @perfect_matched = completed else @completion_state = CompletionState::MENU - complete(list, true) if @config.show_all_if_ambiguous + perform_completion(list, true) if @config.show_all_if_ambiguous end if not just_show_list and target < completed @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding) @@ -951,7 +934,8 @@ class Reline::LineEditor unless @waiting_proc byte_pointer_diff = @byte_pointer - old_byte_pointer @byte_pointer = old_byte_pointer - send(@vi_waiting_operator, byte_pointer_diff) + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) cleanup_waiting end else @@ -1012,7 +996,8 @@ class Reline::LineEditor if @vi_waiting_operator byte_pointer_diff = @byte_pointer - old_byte_pointer @byte_pointer = old_byte_pointer - send(@vi_waiting_operator, byte_pointer_diff) + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) cleanup_waiting end @kill_ring.process @@ -1067,10 +1052,6 @@ class Reline::LineEditor end private def normal_char(key) - 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? @@ -1083,17 +1064,7 @@ class Reline::LineEditor else # single byte return if key.char >= 128 # maybe, first byte of multi byte method_symbol = @config.editing_mode.get_method(key.combined_char) - if key.with_meta and method_symbol == :ed_unassigned - 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 + process_key(key.combined_char, method_symbol) @multibyte_buffer.clear end if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize @@ -1113,6 +1084,7 @@ class Reline::LineEditor end def input_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 @@ -1120,53 +1092,35 @@ class Reline::LineEditor end end if key.char.nil? - if @first_char - @eof = true - end + process_insert(force: true) + @eof = buffer_empty? finish return end - old_lines = @buffer_of_lines.dup - @first_char = false @completion_occurs = false - if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord - 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 - complete(result, false) - end - end - end - elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) - # In vi mode, move completed list even if autocompletion is off - if not @config.disable_completion - process_insert(force: true) - @completion_state = CompletionState::NORMAL - @completion_occurs = move_completed_list("\C-p".ord == key.char ? :up : :down) - end - elsif Symbol === key.char and respond_to?(key.char, true) + + if key.char.is_a?(Symbol) process_key(key.char, key.char) else normal_char(key) end + + @prev_action_state, @next_action_state = @next_action_state, NullActionState + unless @completion_occurs @completion_state = CompletionState::NORMAL @completion_journey_state = nil end + push_input_lines unless @undoing + @undoing = false + if @in_pasting clear_dialogs return end - modified = old_lines != @buffer_of_lines + 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) @@ -1175,6 +1129,29 @@ class Reline::LineEditor modified end + def save_old_buffer + @old_buffer_of_lines = @buffer_of_lines.dup + end + + def push_input_lines + if @old_buffer_of_lines == @buffer_of_lines + @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index] + else + @input_lines = @input_lines[0..@input_lines_position] + @input_lines_position += 1 + @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index]) + end + trim_input_lines + end + + MAX_INPUT_LINES = 100 + def trim_input_lines + if @input_lines.size > MAX_INPUT_LINES + @input_lines.shift + @input_lines_position -= 1 + end + end + def scroll_into_view _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position if wrapped_cursor_y < screen_scroll_top @@ -1251,6 +1228,18 @@ class Reline::LineEditor 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) if Reline.completer_word_break_characters.empty? word_break_regexp = nil @@ -1332,6 +1321,18 @@ class Reline::LineEditor @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end + def insert_multiline_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_input_lines + end + def insert_text(text) if @buffer_of_lines[@line_index].bytesize == @byte_pointer @buffer_of_lines[@line_index] += text @@ -1388,6 +1389,10 @@ class Reline::LineEditor whole_lines.join("\n") end + private def buffer_empty? + current_line.empty? and @buffer_of_lines.size == 1 + end + def finished? @finished end @@ -1430,13 +1435,42 @@ class Reline::LineEditor end end - private def completion_journey_up(key) - if not @config.disable_completion and @config.autocompletion + 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(:up) + @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 - alias_method :menu_complete_backward, :completion_journey_up + + 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. @@ -1707,29 +1741,31 @@ class Reline::LineEditor end private def ed_search_prev_history(key, arg: 1) - substr = current_line.byteslice(0, @byte_pointer) + substr = prev_action_state_value(:search_history) == :empty ? '' : 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) + move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer) arg -= 1 + set_next_action_state(:search_history, :empty) if substr.empty? 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 = current_line.byteslice(0, @byte_pointer) + substr = prev_action_state_value(:search_history) == :empty ? '' : 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? - move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) + move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer) arg -= 1 + set_next_action_state(:search_history, :empty) if substr.empty? ed_search_next_history(key, arg: arg) if arg > 0 end alias_method :history_search_forward, :ed_search_next_history @@ -1885,7 +1921,7 @@ class Reline::LineEditor alias_method :kill_whole_line, :em_kill_line private def em_delete(key) - if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord + if buffer_empty? and key == "\C-d".ord @eof = true finish elsif @byte_pointer < current_line.bytesize @@ -1905,7 +1941,7 @@ class Reline::LineEditor 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 @@ -1930,9 +1966,8 @@ class Reline::LineEditor private def ed_clear_screen(key) Reline::IOGate.clear_screen @screen_size = Reline::IOGate.get_screen_size - @rendered_screen.lines = [] @rendered_screen.base_y = 0 - @rendered_screen.cursor_y = 0 + clear_rendered_screen_cache end alias_method :clear_screen, :ed_clear_screen @@ -2207,9 +2242,11 @@ class Reline::LineEditor 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) + else + return end copy_for_vi(cut) - set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) + set_current_line(line, @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) end private def vi_yank(key, arg: nil) @@ -2228,13 +2265,14 @@ class Reline::LineEditor 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) + else + return end copy_for_vi(cut) end private def vi_list_or_eof(key) - if current_line.empty? and @buffer_of_lines.size == 1 - set_current_line('', 0) + if buffer_empty? @eof = true finish else @@ -2475,4 +2513,36 @@ class Reline::LineEditor private def vi_editing_mode(key) @config.editing_mode = :vi_insert end + + private def undo(_key) + @undoing = true + + return if @input_lines_position <= 0 + + @input_lines_position -= 1 + target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position] + set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y) + end + + private def redo(_key) + @undoing = true + + return if @input_lines_position >= @input_lines.size - 1 + + @input_lines_position += 1 + target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position] + set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y) + end + + private def prev_action_state_value(type) + @prev_action_state[0] == type ? @prev_action_state[1] : nil + end + + private def set_next_action_state(type, value) + @next_action_state = [type, value] + end + + private def re_read_init_file(_key) + @config.reload + end end |