class Reline::LineEditor
Constants
- CompletionJourneyState
- DIALOG_DEFAULT_HEIGHT
- MAX_INPUT_LINES
- MINIMUM_SCROLLBAR_HEIGHT
- NullActionState
- RenderedScreen
- VI_MOTIONS
Attributes
auto_indent_proc
[RW]
byte_pointer
[R]
TODO: Use “private alias_method” idiom after drop Ruby 2.5.
completion_append_character
[RW]
completion_proc
[RW]
confirm_multiline_termination_proc
[RW]
dig_perfect_match_proc
[RW]
output
[W]
output_modifier_proc
[RW]
prompt_proc
[RW]
Public Class Methods
new
(config, encoding)
# File lib/reline/line_editor.rb, line 75 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
Public Instance Methods
add_dialog_proc
(name, p, context = nil)
# File lib/reline/line_editor.rb, line 681 def add_dialog_proc(name, p, context = nil) dialog = Dialog.new(name, @config, DialogProcScope.new(self, @config, p, context)) if index = @dialogs.find_index { |d| d.name == name } @dialogs[index] = dialog else @dialogs << dialog end end
byte_pointer=
(val)
# File lib/reline/line_editor.rb, line 1380 def byte_pointer=(val) @byte_pointer = val end
calculate_overlay_levels
(overlay_levels)
# File lib/reline/line_editor.rb, line 398 def calculate_overlay_levels(overlay_levels) levels = [] overlay_levels.each do |x, w, l| levels.fill(l, x, w) end levels end
call_completion_proc
()
# File lib/reline/line_editor.rb, line 1165 def call_completion_proc result = retrieve_completion_block(true) pre, target, post = result result = call_completion_proc_with_checking_args(pre, target, post) Reline.core.instance_variable_set(:@completion_quote_character, nil) result end
call_completion_proc_with_checking_args
(pre, target, post)
# File lib/reline/line_editor.rb, line 1173 def call_completion_proc_with_checking_args(pre, target, post) if @completion_proc and target argnum = @completion_proc.parameters.inject(0) { |result, item| case item.first when :req, :opt result + 1 when :rest break 3 end } case argnum when 1 result = @completion_proc.(target) when 2 result = @completion_proc.(target, pre) when 3..Float::INFINITY result = @completion_proc.(target, pre, post) end end result end
clear_dialogs
()
# File lib/reline/line_editor.rb, line 446 def clear_dialogs @dialogs.each do |dialog| dialog.contents = nil dialog.trap_key = nil end end
confirm_multiline_termination
()
# File lib/reline/line_editor.rb, line 1319 def confirm_multiline_termination temp_buffer = @buffer_of_lines.dup @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end
current_byte_pointer_cursor
()
# File lib/reline/line_editor.rb, line 304 def current_byte_pointer_cursor calculate_width(current_line.byteslice(0, @byte_pointer)) end
current_line
()
# File lib/reline/line_editor.rb, line 1216 def current_line @buffer_of_lines[@line_index] end
delete_text
(start = nil, length = nil)
# File lib/reline/line_editor.rb, line 1346 def delete_text(start = nil, length = nil) if start.nil? and length.nil? 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 end elsif not start.nil? and not length.nil? 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 = 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 = 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 set_current_line(current_line.byteslice(0, start)) end end
dialog_proc_scope_completion_journey_data
()
# File lib/reline/line_editor.rb, line 888 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
editing_mode
()
# File lib/reline/line_editor.rb, line 790 def editing_mode @config.editing_mode end
eof?
()
# File lib/reline/line_editor.rb, line 218 def eof? @eof end
finalize
()
# File lib/reline/line_editor.rb, line 214 def finalize Signal.trap('INT', @old_trap) end
finish
()
# File lib/reline/line_editor.rb, line 1400 def finish @finished = true @config.reset end
finished?
()
# File lib/reline/line_editor.rb, line 1396 def finished? @finished end
handle_signal
()
# File lib/reline/line_editor.rb, line 166 def handle_signal handle_interrupted handle_resized end
input_key
(key)
# File lib/reline/line_editor.rb, line 1086 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 return end end if key.char.nil? process_insert(force: true) @eof = buffer_empty? finish return end @completion_occurs = false 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_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
insert_multiline_text
(text)
# File lib/reline/line_editor.rb, line 1324 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 + Reline::Unicode.safe_encode(text, @encoding).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
insert_text
(text)
# File lib/reline/line_editor.rb, line 1336 def insert_text(text) if @buffer_of_lines[@line_index].bytesize == @byte_pointer @buffer_of_lines[@line_index] += text else @buffer_of_lines[@line_index] = byteinsert(@buffer_of_lines[@line_index], @byte_pointer, text) end @byte_pointer += text.bytesize process_auto_indent end
io_gate
()
# File lib/reline/line_editor.rb, line 82 def io_gate Reline::IOGate end
line
()
# File lib/reline/line_editor.rb, line 1212 def line() @buffer_of_lines.join("\n") unless eof? end
modified_lines
()
# File lib/reline/line_editor.rb, line 351 def modified_lines with_cache(__method__, whole_lines, finished?) do |whole, complete| modify_lines(whole, complete) end end
multiline_off
()
# File lib/reline/line_editor.rb, line 273 def multiline_off @is_multiline = false end
multiline_on
()
# File lib/reline/line_editor.rb, line 269 def multiline_on @is_multiline = true end
print_nomultiline_prompt
()
# File lib/reline/line_editor.rb, line 471 def print_nomultiline_prompt # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence. @output.write @prompt if @prompt && !@is_multiline end
prompt_list
()
# File lib/reline/line_editor.rb, line 357 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 end
push_input_lines
()
# File lib/reline/line_editor.rb, line 1136 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
render
()
# File lib/reline/line_editor.rb, line 476 def render wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position 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 @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 render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top end
render_finished
()
# File lib/reline/line_editor.rb, line 461 def render_finished 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) wrapped_lines.last.empty? ? "#{line} " : line end @output.puts lines.map { |l| "#{l}\r\n" }.join end
render_line_differential
(old_items, new_items)
# File lib/reline/line_editor.rb, line 406 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_sequence}#{' ' * 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_sequence}#{content}#{Reline::IOGate.reset_color_sequence}" end base_x += width end if old_levels.size > new_levels.size Reline::IOGate.move_cursor_column new_levels.size Reline::IOGate.erase_after_cursor end end
rerender
()
# File lib/reline/line_editor.rb, line 554 def rerender render unless @in_pasting end
reset
(prompt = '', encoding:)
# File lib/reline/line_editor.rb, line 139 def reset(prompt = '', encoding:) @screen_size = Reline::IOGate.get_screen_size reset_variables(prompt, encoding: encoding) @rendered_screen.base_y = Reline::IOGate.cursor_pos.y if ENV.key?('RELINE_ALT_SCROLLBAR') @full_block = '::' @upper_half_block = "''" @lower_half_block = '..' @block_elem_width = 2 elsif Reline::IOGate.win? @full_block = '█' @upper_half_block = '▀' @lower_half_block = '▄' @block_elem_width = 1 elsif @encoding == Encoding::UTF_8 @full_block = '█' @upper_half_block = '▀' @lower_half_block = '▄' @block_elem_width = Reline::Unicode.calculate_width('█') else @full_block = '::' @upper_half_block = "''" @lower_half_block = '..' @block_elem_width = 2 end end
reset_line
()
# File lib/reline/line_editor.rb, line 260 def reset_line @byte_pointer = 0 @buffer_of_lines = [String.new(encoding: @encoding)] @line_index = 0 @cache.clear @line_backup_in_history = nil @multibyte_buffer = String.new(encoding: 'ASCII-8BIT') end
reset_variables
(prompt = '', encoding:)
# File lib/reline/line_editor.rb, line 222 def reset_variables(prompt = '', encoding:) @prompt = prompt.gsub("\n", "\\n") @mark_pointer = nil @encoding = encoding @is_multiline = false @finished = false @history_pointer = nil @kill_ring ||= Reline::KillRing.new @vi_clipboard = '' @vi_arg = nil @waiting_proc = nil @vi_waiting_operator = nil @vi_waiting_operator_arg = nil @completion_journey_state = nil @completion_state = CompletionState::NORMAL @perfect_matched = nil @menu_info = nil @searching_prompt = nil @just_cursor_moving = false @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) @scroll_partial_screen = 0 @drop_terminate_spaces = false @in_pasting = false @auto_indent_proc = nil @dialogs = [] @interrupted = false @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
rest_height
(wrapped_cursor_y)
# File lib/reline/line_editor.rb, line 550 def rest_height(wrapped_cursor_y) screen_height - wrapped_cursor_y + screen_scroll_top - @rendered_screen.base_y - 1 end
retrieve_completion_block
(set_completion_quote_character = false)
# File lib/reline/line_editor.rb, line 1243 def retrieve_completion_block(set_completion_quote_character = false) if Reline.completer_word_break_characters.empty? word_break_regexp = nil else word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ end if Reline.completer_quote_characters.empty? quote_characters_regexp = nil else quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ end before = current_line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil quote = nil closing_quote = nil escaped_quote = nil i = 0 while i < @byte_pointer do slice = current_line.byteslice(i, @byte_pointer - i) unless slice.valid_encoding? i += 1 next end if quote and slice.start_with?(closing_quote) quote = nil i += 1 rest = nil elsif quote and slice.start_with?(escaped_quote) # skip i += 2 elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new " rest = $' quote = $& closing_quote = /(?!\\)#{Regexp.escape(quote)}/ escaped_quote = /\\#{Regexp.escape(quote)}/ i += 1 break_pointer = i - 1 elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 before = current_line.byteslice(i, @byte_pointer - i) break_pointer = i else i += 1 end end postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) if rest 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) if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote insert_text(quote) end end else preposing = '' if break_pointer preposing = current_line.byteslice(0, break_pointer) else preposing = '' end target = before 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
save_old_buffer
()
# File lib/reline/line_editor.rb, line 1132 def save_old_buffer @old_buffer_of_lines = @buffer_of_lines.dup end
screen_height
()
# File lib/reline/line_editor.rb, line 363 def screen_height @screen_size.first end
screen_scroll_top
()
# File lib/reline/line_editor.rb, line 371 def screen_scroll_top @scroll_partial_screen end
screen_width
()
# File lib/reline/line_editor.rb, line 367 def screen_width @screen_size.last end
scroll_into_view
()
# File lib/reline/line_editor.rb, line 1155 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
set_current_line
(line, byte_pointer = nil)
# File lib/reline/line_editor.rb, line 1220 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
set_current_lines
(lines, byte_pointer = nil, line_index = 0)
# File lib/reline/line_editor.rb, line 1231 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
set_pasting_state
(in_pasting)
# File lib/reline/line_editor.rb, line 86 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
set_signal_handlers
()
# File lib/reline/line_editor.rb, line 205 def set_signal_handlers Reline::IOGate.set_winch_handler do @resized = true end @old_trap = Signal.trap('INT') do @interrupted = true end end
trim_input_lines
()
# File lib/reline/line_editor.rb, line 1148 def trim_input_lines if @input_lines.size > MAX_INPUT_LINES @input_lines.shift @input_lines_position -= 1 end end
update
(key)
# File lib/reline/line_editor.rb, line 1076 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
update_dialogs
(key = nil)
# File lib/reline/line_editor.rb, line 453 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
upper_space_height
(wrapped_cursor_y)
# File lib/reline/line_editor.rb, line 546 def upper_space_height(wrapped_cursor_y) wrapped_cursor_y - screen_scroll_top end
whole_buffer
()
# File lib/reline/line_editor.rb, line 1388 def whole_buffer whole_lines.join("\n") end
whole_lines
()
# File lib/reline/line_editor.rb, line 1384 def whole_lines @buffer_of_lines.dup end
with_cache
(key, *deps) { |*deps, cached_deps, value| ... }
# File lib/reline/line_editor.rb, line 343 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
wrap_method_call
(method_symbol, method_obj, key, with_operator = false)
# File lib/reline/line_editor.rb, line 962 def wrap_method_call(method_symbol, method_obj, key, with_operator = false) 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 if @vi_arg and argumentable?(method_obj) if with_operator and inclusive?(method_obj) method_obj.(key, arg: @vi_arg, inclusive: true) else method_obj.(key, arg: @vi_arg) end else if with_operator and inclusive?(method_obj) method_obj.(key, inclusive: true) else method_obj.(key) end end end
wrapped_cursor_position
()
Calculate cursor position in word wrapped content.
# File lib/reline/line_editor.rb, line 437 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
wrapped_prompt_and_input_lines
()
# File lib/reline/line_editor.rb, line 375 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 end