ruby/lib/reline/line_editor.rb

2632 строки
84 KiB
Ruby

require 'reline/kill_ring'
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
attr_accessor :completion_proc
attr_accessor :completion_append_character
attr_accessor :output_modifier_proc
attr_accessor :prompt_proc
attr_accessor :auto_indent_proc
attr_accessor :dig_perfect_match_proc
attr_writer :output
VI_MOTIONS = %i{
ed_prev_char
ed_next_char
vi_zero
ed_move_to_beg
ed_move_to_end
vi_to_column
vi_next_char
vi_prev_char
vi_next_word
vi_prev_word
vi_to_next_char
vi_to_prev_char
vi_end_word
vi_next_big_word
vi_prev_big_word
vi_end_big_word
vi_repeat_next_char
vi_repeat_prev_char
}
module CompletionState
NORMAL = :normal
COMPLETION = :completion
MENU = :menu
MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match
PERFECT_MATCH = :perfect_match
end
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
MINIMUM_SCROLLBAR_HEIGHT = 1
def initialize(config, encoding)
@config = config
@completion_append_character = ''
@screen_size = Reline::IOGate.get_screen_size
reset_variables(encoding: encoding)
end
def io_gate
Reline::IOGate
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
if @config.show_mode_in_prompt
if @config.editing_mode_is?(:vi_command)
@config.vi_cmd_mode_string
elsif @config.editing_mode_is?(:vi_insert)
@config.vi_ins_mode_string
elsif @config.editing_mode_is?(:emacs)
@config.emacs_mode_string
else
'?'
end
end
end
private def check_multiline_prompt(buffer, mode_string)
if @vi_arg
prompt = "(arg: #{@vi_arg}) "
elsif @searching_prompt
prompt = @searching_prompt
else
prompt = @prompt
end
if @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?
prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
prompt = prompt_list[@line_index]
prompt = prompt_list[0] if prompt.nil?
prompt = prompt_list.last if prompt.nil?
if buffer.size > prompt_list.size
(buffer.size - prompt_list.size).times do
prompt_list << prompt_list.last
end
end
prompt_list
else
prompt = mode_string + prompt if mode_string
[prompt] * buffer.size
end
end
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
def handle_signal
handle_interrupted
handle_resized
end
private def handle_resized
return unless @resized
@screen_size = Reline::IOGate.get_screen_size
@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
@old_trap.call if @old_trap.respond_to?(:call)
end
end
def set_signal_handlers
Reline::IOGate.set_winch_handler do
@resized = true
end
@old_trap = Signal.trap('INT') do
@interrupted = true
end
end
def finalize
Signal.trap('INT', @old_trap)
end
def eof?
@eof
end
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
@waiting_operator_proc = nil
@waiting_operator_vi_arg = nil
@completion_journey_state = nil
@completion_state = CompletionState::NORMAL
@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)
@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)
reset_line
end
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
def multiline_on
@is_multiline = true
end
def multiline_off
@is_multiline = false
end
private def insert_new_line(cursor_line, next_line)
@buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
@buffer_of_lines[@line_index] = cursor_line
@line_index += 1
@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 */, '')
)
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 split_by_width(str, max_width)
Reline::Unicode.split_by_width(str, max_width, @encoding)
end
def current_byte_pointer_cursor
calculate_width(current_line.byteslice(0, @byte_pointer))
end
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_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
last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
end_of_line_cursor = new_cursor_max - last_width
else
end_of_line_cursor = new_cursor_max
end
else
end_of_line_cursor = new_cursor_max
end
line_to_calc.grapheme_clusters.each do |gc|
mbchar = gc.encode(Encoding::UTF_8)
mbchar_width = Reline::Unicode.get_mbchar_width(mbchar)
now = new_cursor + mbchar_width
if now > end_of_line_cursor or now > cursor
break
end
new_cursor += mbchar_width
if new_cursor > max_width * height
height += 1
end
new_byte_pointer += gc.bytesize
end
@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 modified_lines
with_cache(__method__, whole_lines, finished?) do |whole, complete|
modify_lines(whole, complete)
end
end
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
def screen_height
@screen_size.first
end
def screen_width
@screen_size.last
end
def screen_scroll_top
@scroll_partial_screen
end
def wrapped_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]
cached_wraps[[prompt, line]] || split_by_width("#{prompt}#{line}", width).first.compact
end
end
end
def calculate_overlay_levels(overlay_levels)
levels = []
overlay_levels.each do |x, w, l|
levels.fill(l, x, w)
end
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]
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}"
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
# 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_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_lines.flatten[screen_scroll_top, screen_height].map do |l|
[[0, Reline::Unicode.calculate_width(l, true), l]]
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] ||= []
dialog_rows[index + 1] = [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 current_row
wrapped_lines.flatten[wrapped_cursor_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
@proc_to_exec = proc_to_exec
@context = context
@cursor_pos = Reline::CursorPos.new
end
def context
@context
end
def retrieve_completion_block(set_completion_quote_character = false)
@line_editor.retrieve_completion_block(set_completion_quote_character)
end
def call_completion_proc_with_checking_args(pre, target, post)
@line_editor.call_completion_proc_with_checking_args(pre, target, post)
end
def set_dialog(dialog)
@dialog = dialog
end
def dialog
@dialog
end
def set_cursor_pos(col, row)
@cursor_pos.x = col
@cursor_pos.y = row
end
def set_key(key)
@key = key
end
def key
@key
end
def cursor_pos
@cursor_pos
end
def just_cursor_moving
@line_editor.instance_variable_get(:@just_cursor_moving)
end
def screen_width
@line_editor.screen_width
end
def screen_height
@line_editor.screen_height
end
def preferred_dialog_height
_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.dialog_proc_scope_completion_journey_data
end
def config
@config
end
def call
instance_exec(&@proc_to_exec)
end
end
class Dialog
attr_reader :name, :contents, :width
attr_accessor :scroll_top, :pointer, :column, :vertical_offset, :trap_key
def initialize(name, config, proc_scope)
@name = name
@config = config
@proc_scope = proc_scope
@width = nil
@scroll_top = 0
@trap_key = nil
end
def set_cursor_pos(col, row)
@proc_scope.set_cursor_pos(col, row)
end
def width=(v)
@width = v
end
def contents=(contents)
@contents = contents
if contents and @width.nil?
@width = contents.map{ |line| Reline::Unicode.calculate_width(line, true) }.max
end
end
def call(key)
@proc_scope.set_dialog(self)
@proc_scope.set_key(key)
dialog_render_info = @proc_scope.call
if @trap_key
if @trap_key.any?{ |i| i.is_a?(Array) } # multiple trap
@trap_key.each do |t|
@config.add_oneshot_key_binding(t, @name)
end
elsif @trap_key.is_a?(Array)
@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
end
end
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
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
[x_range, y_range]
end
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
return
end
contents = dialog_render_info.contents
pointer = dialog.pointer
if dialog_render_info.width
dialog.width = dialog_render_info.width
else
dialog.width = contents.map { |l| calculate_width(l, true) }.max
end
height = dialog_render_info.height || DIALOG_DEFAULT_HEIGHT
height = contents.size if contents.size < height
if contents.size > height
if dialog.pointer
if dialog.pointer < 0
dialog.scroll_top = 0
elsif (dialog.pointer - dialog.scroll_top) >= (height - 1)
dialog.scroll_top = dialog.pointer - (height - 1)
elsif (dialog.pointer - dialog.scroll_top) < 0
dialog.scroll_top = dialog.pointer
end
pointer = dialog.pointer - dialog.scroll_top
else
dialog.scroll_top = 0
end
contents = contents[dialog.scroll_top, height]
end
if dialog_render_info.scrollbar and dialog_render_info.contents.size > height
bar_max_height = height * 2
moving_distance = (dialog_render_info.contents.size - height) * 2
position_ratio = dialog.scroll_top.zero? ? 0.0 : ((dialog.scroll_top * 2).to_f / moving_distance)
bar_height = (bar_max_height * ((contents.size * 2).to_f / (dialog_render_info.contents.size * 2))).floor.to_i
bar_height = MINIMUM_SCROLLBAR_HEIGHT if bar_height < MINIMUM_SCROLLBAR_HEIGHT
scrollbar_pos = ((bar_max_height - bar_height) * position_ratio).floor.to_i
else
scrollbar_pos = nil
end
dialog.column = dialog_render_info.pos.x
dialog.width += @block_elem_width if scrollbar_pos
diff = (dialog.column + dialog.width) - screen_width
if diff > 0
dialog.column -= diff
end
if rest_height(screen_scroll_top + cursor_row) - dialog_render_info.pos.y >= height
dialog.vertical_offset = dialog_render_info.pos.y + 1
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_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|
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 = "#{line_sgr}#{str}"
if scrollbar_pos
if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height)
colored_content + scrollbar_sgr + @full_block
elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height)
colored_content + scrollbar_sgr + @upper_half_block
elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height)
colored_content + scrollbar_sgr + @lower_half_block
else
colored_content + scrollbar_sgr + ' ' * @block_elem_width
end
else
colored_content
end
end
end
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.map { |l| Reline::Unicode.escape_for_print(l) }
end
end
def editing_mode
@config.editing_mode
end
private def menu(_target, list)
@menu_info = MenuInfo.new(list)
end
private def complete_internal_proc(list, is_menu)
preposing, target, postposing = retrieve_completion_block
list = list.select { |i|
if i and not Encoding.compatible?(target.encoding, i.encoding)
raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}"
end
if @config.completion_ignore_case
i&.downcase&.start_with?(target.downcase)
else
i&.start_with?(target)
end
}.uniq
if is_menu
menu(target, list)
return nil
end
completed = list.inject { |memo, item|
begin
memo_mbchars = memo.unicode_normalize.grapheme_clusters
item_mbchars = item.unicode_normalize.grapheme_clusters
rescue Encoding::CompatibilityError
memo_mbchars = memo.grapheme_clusters
item_mbchars = item.grapheme_clusters
end
size = [memo_mbchars.size, item_mbchars.size].min
result = +''
size.times do |i|
if @config.completion_ignore_case
if memo_mbchars[i].casecmp?(item_mbchars[i])
result << memo_mbchars[i]
else
break
end
else
if memo_mbchars[i] == item_mbchars[i]
result << memo_mbchars[i]
else
break
end
end
end
result
}
[target, preposing, completed, postposing]
end
private def complete(list, just_show_list)
case @completion_state
when CompletionState::NORMAL
@completion_state = CompletionState::COMPLETION
when CompletionState::PERFECT_MATCH
@dig_perfect_match_proc&.(@perfect_matched)
end
if just_show_list
is_menu = true
elsif @completion_state == CompletionState::MENU
is_menu = true
elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
is_menu = true
else
is_menu = false
end
result = complete_internal_proc(list, is_menu)
if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
@completion_state = CompletionState::PERFECT_MATCH
end
return if result.nil?
target, preposing, completed, postposing = result
return if completed.nil?
if target <= completed and (@completion_state == CompletionState::COMPLETION)
if list.include?(completed)
if list.one?
@completion_state = CompletionState::PERFECT_MATCH
else
@completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
end
@perfect_matched = completed
else
@completion_state = CompletionState::MENU
end
if not just_show_list and target < completed
@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
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_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_MOTIONS.include?(method_symbol)
old_byte_pointer = @byte_pointer
@vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg&.> 1
block.(true)
unless @waiting_proc
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
@waiting_operator_proc.(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_byte_pointer = @byte_pointer
old_waiting_proc.(k)
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
current_waiting_operator_proc.(byte_pointer_diff)
@waiting_operator_proc = old_waiting_operator_proc
}
end
else
# Ignores operator when not motion is given.
block.(false)
end
@waiting_operator_proc = nil
@waiting_operator_vi_arg = nil
if @vi_arg
@vi_arg = nil
end
else
block.(false)
end
end
private def argumentable?(method_obj)
method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg }
end
private def inclusive?(method_obj)
# If a motion method with the keyword argument "inclusive" follows the
# operator, it must contain the character at the cursor position.
method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive }
end
def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil?
not_insertion = method_symbol != :ed_insert
process_insert(force: not_insertion)
end
if @vi_arg and argumentable?(method_obj)
if with_operator and inclusive?(method_obj)
method_obj.(key, arg: @vi_arg, inclusive: true)
else
method_obj.(key, arg: @vi_arg)
end
else
if with_operator and inclusive?(method_obj)
method_obj.(key, inclusive: true)
else
method_obj.(key)
end
end
end
private def process_key(key, method_symbol)
if method_symbol and respond_to?(method_symbol, true)
method_obj = method(method_symbol)
else
method_obj = nil
end
if method_symbol and key.is_a?(Symbol)
if @vi_arg and argumentable?(method_obj)
run_for_operators(key, method_symbol) do |with_operator|
wrap_method_call(method_symbol, method_obj, key, with_operator)
end
else
wrap_method_call(method_symbol, method_obj, key) if method_obj
end
@kill_ring.process
if @vi_arg
@vi_arg = nil
end
elsif @vi_arg
if key.chr =~ /[0-9]/
ed_argument_digit(key)
else
if argumentable?(method_obj)
run_for_operators(key, method_symbol) do |with_operator|
wrap_method_call(method_symbol, method_obj, key, with_operator)
end
elsif @waiting_proc
@waiting_proc.(key)
elsif method_obj
wrap_method_call(method_symbol, method_obj, key)
else
ed_insert(key) unless @config.editing_mode_is?(:vi_command)
end
@kill_ring.process
if @vi_arg
@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)
else
run_for_operators(key, method_symbol) do |with_operator|
wrap_method_call(method_symbol, method_obj, key, with_operator)
end
end
@kill_ring.process
else
ed_insert(key) unless @config.editing_mode_is?(:vi_command)
end
end
private def normal_char(key)
if key.combined_char.is_a?(Symbol)
process_key(key.combined_char, key.combined_char)
return
end
@multibyte_buffer << key.combined_char
if @multibyte_buffer.size > 1
if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil)
@multibyte_buffer.clear
else
# invalid
return
end
else # single byte
return if key.char >= 128 # maybe, first byte of multi byte
method_symbol = @config.editing_mode.get_method(key.combined_char)
if key.with_meta and method_symbol == :ed_unassigned
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 @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
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)
@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?
if @first_char
@eof = true
end
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?(:emacs, :vi_insert) and key.char == :completion_journey_up
if not @config.disable_completion and @config.autocompletion
process_insert(force: true)
@completion_state = CompletionState::NORMAL
completion_occurs = move_completed_list(:up)
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)
process_key(key.char, key.char)
else
normal_char(key)
end
unless completion_occurs
@completion_state = CompletionState::NORMAL
@completion_journey_state = nil
end
if @in_pasting
clear_dialogs
return
end
modified = old_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 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
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
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
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
end
def line()
current_line 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 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
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
end
[preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
end
def confirm_multiline_termination
temp_buffer = @buffer_of_lines.dup
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
end
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
def delete_text(start = nil, length = nil)
if start.nil? and length.nil?
if @is_multiline
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
else
set_current_line('', 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
def byte_pointer=(val)
@byte_pointer = val
end
def whole_lines
@buffer_of_lines.dup
end
def whole_buffer
whole_lines.join("\n")
end
def finished?
@finished
end
def finish
@finished = true
@config.reset
end
private def byteslice!(str, byte_pointer, size)
new_str = str.byteslice(0, byte_pointer)
new_str << str.byteslice(byte_pointer + size, str.bytesize)
[new_str, str.byteslice(byte_pointer, size)]
end
private def byteinsert(str, byte_pointer, other)
new_str = str.byteslice(0, byte_pointer)
new_str << other
new_str << str.byteslice(byte_pointer, str.bytesize)
new_str
end
private def calculate_width(str, allow_escape_code = false)
Reline::Unicode.calculate_width(str, allow_escape_code)
end
private def key_delete(key)
if @config.editing_mode_is?(:vi_insert)
ed_delete_next_char(key)
elsif @config.editing_mode_is?(:emacs)
em_delete(key)
end
end
private def key_newline(key)
if @is_multiline
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)
end
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)
insert_text(@continuous_insertion_buffer)
@continuous_insertion_buffer.clear
end
# Editline:: +ed-insert+ (vi input: almost all; emacs: printable characters)
# In insert mode, insert the input character left of the cursor
# position. In replace mode, overwrite the character at the
# cursor and move the cursor to the right by one character
# position. Accept an argument to do this repeatedly. It is an
# error if the input character is the NUL character (+Ctrl-@+).
# Failure to enlarge the edit buffer also results in an error.
# Editline:: +ed-digit+ (emacs: 0 to 9) If in argument input mode, append
# the input digit to the argument being read. Otherwise, call
# +ed-insert+. It is an error if the input character is not a
# digit or if the existing argument is already greater than a
# million.
# GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself.
private def ed_insert(key)
if key.instance_of?(String)
begin
key.encode(Encoding::UTF_8)
rescue Encoding::UndefinedConversionError
return
end
str = key
else
begin
key.chr.encode(Encoding::UTF_8)
rescue Encoding::UndefinedConversionError
return
end
str = key.chr
end
if @in_pasting
@continuous_insertion_buffer << str
return
elsif not @continuous_insertion_buffer.empty?
process_insert
end
insert_text(str)
end
alias_method :ed_digit, :ed_insert
alias_method :self_insert, :ed_insert
private def ed_quoted_insert(str, arg: 1)
@waiting_proc = proc { |key|
arg.times do
if key == "\C-j".ord or key == "\C-m".ord
key_newline(key)
elsif key == 0
# Ignore NUL.
else
ed_insert(key)
end
end
@waiting_proc = nil
}
end
alias_method :quoted_insert, :ed_quoted_insert
private def ed_next_char(key, arg: 1)
byte_size = Reline::Unicode.get_next_mbchar_size(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 == current_line.bytesize and @line_index < @buffer_of_lines.size - 1
@byte_pointer = 0
@line_index += 1
end
arg -= 1
ed_next_char(key, arg: arg) if arg > 0
end
alias_method :forward_char, :ed_next_char
private def ed_prev_char(key, arg: 1)
if @byte_pointer > 0
byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
@byte_pointer -= byte_size
elsif @is_multiline and @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
end
alias_method :backward_char, :ed_prev_char
private def vi_first_print(key)
@byte_pointer, = Reline::Unicode.vi_first_print(current_line)
end
private def ed_move_to_beg(key)
@byte_pointer = 0
end
alias_method :beginning_of_line, :ed_move_to_beg
private def ed_move_to_end(key)
@byte_pointer = 0
while @byte_pointer < current_line.bytesize
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
@byte_pointer += byte_size
end
end
alias_method :end_of_line, :ed_move_to_end
private def generate_searcher
Fiber.new do |first_key|
prev_search_key = first_key
search_word = String.new(encoding: @encoding)
multibyte_buf = String.new(encoding: 'ASCII-8BIT')
last_hit = nil
case first_key
when "\C-r".ord
prompt_name = 'reverse-i-search'
when "\C-s".ord
prompt_name = 'i-search'
end
loop do
key = Fiber.yield(search_word)
search_again = false
case key
when -1 # determined
Reline.last_incremental_search = search_word
break
when "\C-h".ord, "\C-?".ord
grapheme_clusters = search_word.grapheme_clusters
if grapheme_clusters.size > 0
grapheme_clusters.pop
search_word = grapheme_clusters.join
end
when "\C-r".ord, "\C-s".ord
search_again = true if prev_search_key == key
prev_search_key = key
else
multibyte_buf << key
if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
search_word << multibyte_buf.dup.force_encoding(@encoding)
multibyte_buf.clear
end
end
hit = nil
if not search_word.empty? and @line_backup_in_history&.include?(search_word)
@history_pointer = nil
hit = @line_backup_in_history
else
if search_again
if search_word.empty? and Reline.last_incremental_search
search_word = Reline.last_incremental_search
end
if @history_pointer
case prev_search_key
when "\C-r".ord
history_pointer_base = 0
history = Reline::HISTORY[0..(@history_pointer - 1)]
when "\C-s".ord
history_pointer_base = @history_pointer + 1
history = Reline::HISTORY[(@history_pointer + 1)..-1]
end
else
history_pointer_base = 0
history = Reline::HISTORY
end
elsif @history_pointer
case prev_search_key
when "\C-r".ord
history_pointer_base = 0
history = Reline::HISTORY[0..@history_pointer]
when "\C-s".ord
history_pointer_base = @history_pointer
history = Reline::HISTORY[@history_pointer..-1]
end
else
history_pointer_base = 0
history = Reline::HISTORY
end
case prev_search_key
when "\C-r".ord
hit_index = history.rindex { |item|
item.include?(search_word)
}
when "\C-s".ord
hit_index = history.index { |item|
item.include?(search_word)
}
end
if hit_index
@history_pointer = history_pointer_base + hit_index
hit = Reline::HISTORY[@history_pointer]
end
end
case prev_search_key
when "\C-r".ord
prompt_name = 'reverse-i-search'
when "\C-s".ord
prompt_name = 'i-search'
end
if hit
if @is_multiline
@buffer_of_lines = hit.split("\n")
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
@line_index = @buffer_of_lines.size - 1
@byte_pointer = current_line.bytesize
@searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
else
@buffer_of_lines = [hit]
@byte_pointer = hit.bytesize
@searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit]
end
last_hit = hit
else
if @is_multiline
@searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word]
else
@searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit]
end
end
end
end
end
private def incremental_search_history(key)
unless @history_pointer
if @is_multiline
@line_backup_in_history = whole_buffer
else
@line_backup_in_history = current_line
end
end
searcher = generate_searcher
searcher.resume(key)
@searching_prompt = "(reverse-i-search)`': "
termination_keys = ["\C-j".ord]
termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
@waiting_proc = ->(k) {
case k
when *termination_keys
if @history_pointer
buffer = Reline::HISTORY[@history_pointer]
else
buffer = @line_backup_in_history
end
if @is_multiline
@buffer_of_lines = buffer.split("\n")
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
@line_index = @buffer_of_lines.size - 1
else
@buffer_of_lines = [buffer]
end
@searching_prompt = nil
@waiting_proc = nil
@byte_pointer = 0
searcher.resume(-1)
when "\C-g".ord
if @is_multiline
@buffer_of_lines = @line_backup_in_history.split("\n")
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
@line_index = @buffer_of_lines.size - 1
else
@buffer_of_lines = [@line_backup_in_history]
end
@history_pointer = nil
@searching_prompt = nil
@waiting_proc = nil
@line_backup_in_history = nil
@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)
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
else
@line_backup_in_history = current_line
@buffer_of_lines = [line]
end
@searching_prompt = nil
@waiting_proc = nil
@byte_pointer = 0
searcher.resume(-1)
end
end
}
end
private def vi_search_prev(key)
incremental_search_history(key)
end
alias_method :reverse_search_history, :vi_search_prev
private def vi_search_next(key)
incremental_search_history(key)
end
alias_method :forward_search_history, :vi_search_next
private def ed_search_prev_history(key, arg: 1)
history = nil
h_pointer = nil
line_no = nil
substr = current_line.slice(0, @byte_pointer)
if @history_pointer.nil?
return if not current_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
cursor = current_byte_pointer_cursor
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
calculate_nearest_cursor(cursor)
else
@buffer_of_lines = [Reline::HISTORY[@history_pointer]]
calculate_nearest_cursor(cursor)
end
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 = current_line.slice(0, @byte_pointer)
if @history_pointer.nil?
return
elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
return
end
history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
h_pointer = nil
line_no = nil
if @is_multiline
h_pointer = history.index { |h|
h.split("\n").each_with_index { |l, i|
if l.start_with?(substr)
line_no = i
break
end
}
not line_no.nil?
}
else
h_pointer = history.index { |l|
l.start_with?(substr)
}
end
h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
return if h_pointer.nil? and not substr.empty?
@history_pointer = h_pointer
if @is_multiline
if @history_pointer.nil? and substr.empty?
@buffer_of_lines = []
@line_index = 0
@byte_pointer = 0
else
cursor = current_byte_pointer_cursor
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
@line_index = line_no
calculate_nearest_cursor(cursor)
end
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
else
if @history_pointer.nil? and substr.empty?
set_current_line('', 0)
else
set_current_line(Reline::HISTORY[@history_pointer])
end
end
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
cursor = current_byte_pointer_cursor
@line_index -= 1
calculate_nearest_cursor(cursor)
return
end
if Reline::HISTORY.empty?
return
end
if @history_pointer.nil?
@history_pointer = Reline::HISTORY.size - 1
cursor = current_byte_pointer_cursor
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
calculate_nearest_cursor(cursor)
else
@line_backup_in_history = whole_buffer
@buffer_of_lines = [Reline::HISTORY[@history_pointer]]
calculate_nearest_cursor(cursor)
end
elsif @history_pointer.zero?
return
else
if @is_multiline
Reline::HISTORY[@history_pointer] = whole_buffer
@history_pointer -= 1
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
@line_index = @buffer_of_lines.size - 1
else
Reline::HISTORY[@history_pointer] = whole_buffer
@history_pointer -= 1
@buffer_of_lines = [Reline::HISTORY[@history_pointer]]
end
end
if @config.editing_mode_is?(:emacs, :vi_insert)
@byte_pointer = current_line.bytesize
elsif @config.editing_mode_is?(:vi_command)
@byte_pointer = 0
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)
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
else
@history_pointer = nil
@buffer_of_lines = [@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
else
Reline::HISTORY[@history_pointer] = whole_buffer
@history_pointer += 1
@buffer_of_lines = [Reline::HISTORY[@history_pointer]]
end
end
if @config.editing_mode_is?(:emacs, :vi_insert)
@byte_pointer = current_line.bytesize
elsif @config.editing_mode_is?(:vi_command)
@byte_pointer = 0
end
arg -= 1
ed_next_history(key, arg: arg) if arg > 0
end
alias_method :next_history, :ed_next_history
private def ed_newline(key)
process_insert(force: true)
if @is_multiline
if @config.editing_mode_is?(:vi_command)
if @line_index < (@buffer_of_lines.size - 1)
ed_next_history(key) # means cursor down
else
# should check confirm_multiline_termination to finish?
finish
end
else
if @line_index == (@buffer_of_lines.size - 1)
if confirm_multiline_termination
finish
else
key_newline(key)
end
else
# should check confirm_multiline_termination to finish?
@line_index = @buffer_of_lines.size - 1
@byte_pointer = current_line.bytesize
finish
end
end
else
if @history_pointer
Reline::HISTORY[@history_pointer] = whole_buffer
@history_pointer = nil
end
finish
end
end
private def em_delete_prev_char(key, arg: 1)
arg.times do
if @is_multiline and @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
process_auto_indent
end
alias_method :backward_delete_char, :em_delete_prev_char
# Editline:: +ed-kill-line+ (vi command: +D+, +Ctrl-K+; emacs: +Ctrl-K+,
# +Ctrl-U+) + Kill from the cursor to the end of the line.
# GNU Readline:: +kill-line+ (+C-k+) Kill the text from point to the end of
# 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 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 == 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.
# GNU Readline:: +unix-line-discard+ (+C-u+) Kill backward from the cursor
# to the beginning of the current line.
private def vi_kill_line_prev(key)
if @byte_pointer > 0
line, deleted = byteslice!(current_line, 0, @byte_pointer)
set_current_line(line, 0)
@kill_ring.append(deleted, true)
end
end
alias_method :unix_line_discard, :vi_kill_line_prev
# Editline:: +em-kill-line+ (not bound) Delete the entire contents of the
# edit buffer and save it to the cut buffer. +vi-kill-line-prev+
# 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 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 current_line.empty? and (not @is_multiline or @buffer_of_lines.size == 1) and key == "\C-d".ord
@eof = true
finish
elsif @byte_pointer < current_line.bytesize
splitted_last = current_line.byteslice(@byte_pointer, current_line.bytesize)
mbchar = splitted_last.grapheme_clusters.first
line, = byteslice!(current_line, @byte_pointer, mbchar.bytesize)
set_current_line(line)
elsif @is_multiline and @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 current_line.empty? or @byte_pointer < current_line.bytesize
em_delete(key)
elsif !@config.autocompletion # show completed list
result = call_completion_proc
if result.is_a?(Array)
complete(result, true)
end
end
end
alias_method :delete_char_or_list, :em_delete_or_list
private def em_yank(key)
yanked = @kill_ring.yank
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
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)
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 current_line.bytesize > @byte_pointer
byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
@byte_pointer += byte_size
end
end
alias_method :forward_word, :em_next_word
private def ed_prev_word(key)
if @byte_pointer > 0
byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
@byte_pointer -= byte_size
end
end
alias_method :backward_word, :ed_prev_word
private def em_delete_next_word(key)
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)
end
end
alias_method :kill_word, :em_delete_next_word
private def ed_delete_prev_word(key)
if @byte_pointer > 0
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)
end
end
alias_method :backward_kill_word, :ed_delete_prev_word
private def ed_transpose_chars(key)
if @byte_pointer > 0
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(current_line, @byte_pointer)
if (@byte_pointer - back1_byte_size) > 0
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!(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(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?
from_head_to_left_word = before + right_word + middle + 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 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 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 = 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 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 = 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, _ = 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
alias_method :unix_word_rubout, :em_kill_region
private def copy_for_vi(text)
if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
@vi_clipboard = text
end
end
private def vi_insert(key)
@config.editing_mode = :vi_insert
end
private def vi_add(key)
@config.editing_mode = :vi_insert
ed_next_char(key)
end
private def vi_command_mode(key)
ed_prev_char(key)
@config.editing_mode = :vi_command
end
alias_method :vi_movement_mode, :vi_command_mode
private def vi_next_word(key, arg: 1)
if current_line.bytesize > @byte_pointer
byte_size, _ = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces)
@byte_pointer += byte_size
end
arg -= 1
vi_next_word(key, arg: arg) if arg > 0
end
private def vi_prev_word(key, arg: 1)
if @byte_pointer > 0
byte_size, _ = Reline::Unicode.vi_backward_word(current_line, @byte_pointer)
@byte_pointer -= byte_size
end
arg -= 1
vi_prev_word(key, arg: arg) if arg > 0
end
private def vi_end_word(key, arg: 1, inclusive: false)
if current_line.bytesize > @byte_pointer
byte_size, _ = Reline::Unicode.vi_forward_end_word(current_line, @byte_pointer)
@byte_pointer += byte_size
end
arg -= 1
if inclusive and arg.zero?
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
if byte_size > 0
@byte_pointer += byte_size
end
end
vi_end_word(key, arg: arg) if arg > 0
end
private def vi_next_big_word(key, arg: 1)
if current_line.bytesize > @byte_pointer
byte_size, _ = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer)
@byte_pointer += byte_size
end
arg -= 1
vi_next_big_word(key, arg: arg) if arg > 0
end
private def vi_prev_big_word(key, arg: 1)
if @byte_pointer > 0
byte_size, _ = Reline::Unicode.vi_big_backward_word(current_line, @byte_pointer)
@byte_pointer -= byte_size
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 current_line.bytesize > @byte_pointer
byte_size, _ = Reline::Unicode.vi_big_forward_end_word(current_line, @byte_pointer)
@byte_pointer += byte_size
end
arg -= 1
if inclusive and arg.zero?
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
if byte_size > 0
@byte_pointer += byte_size
end
end
vi_end_big_word(key, arg: arg) if arg > 0
end
private def vi_delete_prev_char(key)
if @is_multiline and @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
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, _ = byteslice!(current_line, @byte_pointer, byte_size)
set_current_line(line)
end
end
private def vi_insert_at_bol(key)
ed_move_to_beg(key)
@config.editing_mode = :vi_insert
end
private def vi_add_at_eol(key)
ed_move_to_end(key)
@config.editing_mode = :vi_insert
end
private def ed_delete_prev_char(key, arg: 1)
deleted = +''
arg.times do
if @byte_pointer > 0
byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
@byte_pointer -= byte_size
line, mbchar = byteslice!(current_line, @byte_pointer, byte_size)
set_current_line(line)
deleted.prepend(mbchar)
end
end
copy_for_vi(deleted)
end
private def vi_zero(key)
@byte_pointer = 0
end
private def vi_change_meta(key, arg: 1)
@drop_terminate_spaces = true
@waiting_operator_proc = proc { |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
set_current_line(line)
copy_for_vi(cut)
@byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
@config.editing_mode = :vi_insert
@drop_terminate_spaces = false
}
@waiting_operator_vi_arg = arg
end
private def vi_delete_meta(key, arg: 1)
@waiting_operator_proc = proc { |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))
}
@waiting_operator_vi_arg = arg
end
private def vi_yank(key, arg: 1)
@waiting_operator_proc = proc { |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)
}
@waiting_operator_vi_arg = arg
end
private def vi_list_or_eof(key)
if (not @is_multiline and current_line.empty?) or (@is_multiline and current_line.empty? and @buffer_of_lines.size == 1)
set_current_line('', 0)
@eof = true
finish
else
ed_newline(key)
end
end
alias_method :vi_end_of_transmission, :vi_list_or_eof
alias_method :vi_eof_maybe, :vi_list_or_eof
private def ed_delete_next_char(key, arg: 1)
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
unless current_line.empty? || byte_size == 0
line, mbchar = byteslice!(current_line, @byte_pointer, byte_size)
copy_for_vi(mbchar)
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
ed_delete_next_char(key, arg: arg) if arg > 0
end
private def vi_to_history_line(key)
if Reline::HISTORY.empty?
return
end
if @history_pointer.nil?
@history_pointer = 0
@line_backup_in_history = current_line
set_current_line(Reline::HISTORY[@history_pointer], 0)
elsif @history_pointer.zero?
return
else
Reline::HISTORY[@history_pointer] = current_line
@history_pointer = 0
set_current_line(Reline::HISTORY[@history_pointer], 0)
end
end
private def vi_histedit(key)
path = Tempfile.open { |fp|
if @is_multiline
fp.write whole_lines.join("\n")
else
fp.write current_line
end
fp.path
}
system("#{ENV['EDITOR']} #{path}")
if @is_multiline
@buffer_of_lines = File.read(path).split("\n")
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
@line_index = 0
else
@buffer_of_lines = File.read(path).split("\n")
end
finish
end
private def vi_paste_prev(key, arg: 1)
if @vi_clipboard.size > 0
cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
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
end
private def vi_paste_next(key, arg: 1)
if @vi_clipboard.size > 0
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
end
private def ed_argument_digit(key)
if @vi_arg.nil?
if key.chr.to_i.zero?
if key.anybits?(0b10000000)
unescaped_key = key ^ 0b10000000
unless unescaped_key.chr.to_i.zero?
@vi_arg = unescaped_key.chr.to_i
end
end
else
@vi_arg = key.chr.to_i
end
else
@vi_arg = @vi_arg * 10 + key.chr.to_i
end
end
private def vi_to_column(key, arg: 0)
current_row_width = calculate_width(current_row)
@byte_pointer, = current_line.grapheme_clusters.inject([0, 0]) { |total, gc|
# total has [byte_size, cursor]
mbchar_width = Reline::Unicode.get_mbchar_width(gc)
if (total.last + mbchar_width) >= arg
break total
elsif (total.last + mbchar_width) >= current_row_width
break total
else
total = [total.first + gc.bytesize, total.last + mbchar_width]
total
end
}
end
private def vi_replace_char(key, arg: 1)
@waiting_proc = ->(k) {
if arg == 1
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
before = current_line.byteslice(0, @byte_pointer)
remaining_point = @byte_pointer + byte_size
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(current_line, @byte_pointer + byte_size)
end
before = current_line.byteslice(0, @byte_pointer)
remaining_point = @byte_pointer + byte_size
after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point)
replaced = k.chr * arg
set_current_line(before + replaced + after, @byte_pointer + replaced.bytesize)
@waiting_proc = nil
end
}
end
private def vi_next_char(key, arg: 1, inclusive: false)
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) }
end
private def vi_to_next_char(key, arg: 1, inclusive: false)
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) }
end
private def search_next_char(key, arg, need_prev_char: false, inclusive: false)
if key.instance_of?(String)
inputed_char = key
else
inputed_char = key.chr
end
prev_total = nil
total = nil
found = false
current_line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
# total has [byte_size, cursor]
unless total
# skip cursor point
width = Reline::Unicode.get_mbchar_width(mbchar)
total = [mbchar.bytesize, width]
else
if inputed_char == mbchar
arg -= 1
if arg.zero?
found = true
break
end
end
width = Reline::Unicode.get_mbchar_width(mbchar)
prev_total = total
total = [total.first + mbchar.bytesize, total.last + width]
end
end
if not need_prev_char and found and total
byte_size, _ = total
@byte_pointer += byte_size
elsif need_prev_char and found and prev_total
byte_size, _ = prev_total
@byte_pointer += byte_size
end
if inclusive
byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
if byte_size > 0
@byte_pointer += byte_size
end
end
@waiting_proc = nil
end
private def vi_prev_char(key, arg: 1)
@waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
end
private def vi_to_prev_char(key, arg: 1)
@waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
end
private def search_prev_char(key, arg, need_next_char = false)
if key.instance_of?(String)
inputed_char = key
else
inputed_char = key.chr
end
prev_total = nil
total = nil
found = false
current_line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
# total has [byte_size, cursor]
unless total
# skip cursor point
width = Reline::Unicode.get_mbchar_width(mbchar)
total = [mbchar.bytesize, width]
else
if inputed_char == mbchar
arg -= 1
if arg.zero?
found = true
break
end
end
width = Reline::Unicode.get_mbchar_width(mbchar)
prev_total = total
total = [total.first + mbchar.bytesize, total.last + width]
end
end
if not need_next_char and found and total
byte_size, _ = total
@byte_pointer -= byte_size
elsif need_next_char and found and prev_total
byte_size, _ = prev_total
@byte_pointer -= byte_size
end
@waiting_proc = nil
end
private def vi_join_lines(key, arg: 1)
if @is_multiline and @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
end
private def em_set_mark(key)
@mark_pointer = [@byte_pointer, @line_index]
end
alias_method :set_mark, :em_set_mark
private def em_exchange_mark(key)
return unless @mark_pointer
new_pointer = [@byte_pointer, @line_index]
@byte_pointer, @line_index = @mark_pointer
@mark_pointer = new_pointer
end
alias_method :exchange_point_and_mark, :em_exchange_mark
private def em_meta_next(key)
end
end