Created
December 6, 2020 12:58
-
-
Save ima1zumi/de1d32f5b600a43d8a055c0ea9a3f2cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'reline/kill_ring' | |
require 'reline/unicode' | |
require 'pry-byebug' | |
require 'tempfile' | |
class Reline::LineEditor | |
# TODO: undo | |
attr_reader :line | |
attr_reader :byte_pointer | |
attr_accessor :confirm_multiline_termination_proc | |
attr_accessor :completion_proc | |
attr_accessor :completion_append_character | |
attr_accessor :output_modifier_proc | |
attr_accessor :prompt_proc | |
attr_accessor :auto_indent_proc | |
attr_accessor :pre_input_hook | |
attr_accessor :dig_perfect_match_proc | |
attr_writer :output | |
VI_MOTIONS = %i{ | |
ed_prev_char | |
ed_next_char | |
vi_zero | |
ed_move_to_beg | |
ed_move_to_end | |
vi_to_column | |
vi_next_char | |
vi_prev_char | |
vi_next_word | |
vi_prev_word | |
vi_to_next_char | |
vi_to_prev_char | |
vi_end_word | |
vi_next_big_word | |
vi_prev_big_word | |
vi_end_big_word | |
vi_repeat_next_char | |
vi_repeat_prev_char | |
} | |
module CompletionState | |
NORMAL = :normal | |
COMPLETION = :completion | |
MENU = :menu | |
JOURNEY = :journey | |
MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match | |
PERFECT_MATCH = :perfect_match | |
end | |
CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer) | |
MenuInfo = Struct.new('MenuInfo', :target, :list) | |
PROMPT_LIST_CACHE_TIMEOUT = 0.5 | |
def initialize(config, encoding) | |
@config = config | |
@completion_append_character = '' | |
reset_variables(encoding: encoding) | |
end | |
def simplified_rendering? | |
if finished? | |
false | |
elsif @just_cursor_moving and not @rerender_all | |
true | |
else | |
not @rerender_all and not finished? and Reline::IOGate.in_pasting? | |
end | |
end | |
private def check_multiline_prompt(buffer, prompt) | |
if @vi_arg | |
prompt = "(arg: #{@vi_arg}) " | |
@rerender_all = true | |
elsif @searching_prompt | |
prompt = @searching_prompt | |
@rerender_all = true | |
else | |
prompt = @prompt | |
end | |
return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] if simplified_rendering? | |
if @prompt_proc | |
if @cached_prompt_list and Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size | |
prompt_list = @cached_prompt_list | |
else | |
prompt_list = @cached_prompt_list = @prompt_proc.(buffer) | |
@prompt_cache_time = Time.now.to_f | |
end | |
prompt_list.map!{ prompt } if @vi_arg or @searching_prompt | |
if @config.show_mode_in_prompt | |
if @config.editing_mode_is?(:vi_command) | |
mode_icon = @config.vi_cmd_mode_icon | |
elsif @config.editing_mode_is?(:vi_insert) | |
mode_icon = @config.vi_ins_mode_icon | |
elsif @config.editing_mode_is?(:emacs) | |
mode_icon = @config.emacs_mode_string | |
else | |
mode_icon = '?' | |
end | |
prompt_list.map!{ |pr| mode_icon + pr } | |
end | |
prompt = prompt_list[@line_index] | |
prompt_width = calculate_width(prompt, true) | |
[prompt, prompt_width, prompt_list] | |
else | |
prompt_width = calculate_width(prompt, true) | |
if @config.show_mode_in_prompt | |
if @config.editing_mode_is?(:vi_command) | |
mode_icon = @config.vi_cmd_mode_icon | |
elsif @config.editing_mode_is?(:vi_insert) | |
mode_icon = @config.vi_ins_mode_icon | |
elsif @config.editing_mode_is?(:emacs) | |
mode_icon = @config.emacs_mode_string | |
else | |
mode_icon = '?' | |
end | |
prompt = mode_icon + prompt | |
end | |
[prompt, prompt_width, nil] | |
end | |
end | |
def reset(prompt = '', encoding:) | |
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y | |
@screen_size = Reline::IOGate.get_screen_size | |
reset_variables(prompt, encoding: encoding) | |
@old_trap = Signal.trap('SIGINT') { | |
@old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT" | |
raise Interrupt | |
} | |
Reline::IOGate.set_winch_handler do | |
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y | |
old_screen_size = @screen_size | |
@screen_size = Reline::IOGate.get_screen_size | |
if old_screen_size.last < @screen_size.last # columns increase | |
@rerender_all = true | |
rerender | |
else | |
back = 0 | |
new_buffer = whole_lines | |
prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt) | |
new_buffer.each_with_index do |line, index| | |
prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc | |
width = prompt_width + calculate_width(line) | |
height = calculate_height_by_width(width) | |
back += height | |
end | |
@highest_in_all = back | |
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) | |
@first_line_started_from = | |
if @line_index.zero? | |
0 | |
else | |
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) | |
end | |
if @prompt_proc | |
prompt = prompt_list[@line_index] | |
prompt_width = calculate_width(prompt, true) | |
end | |
calculate_nearest_cursor | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) | |
@rerender_all = true | |
end | |
end | |
end | |
def finalize | |
Signal.trap('SIGINT', @old_trap) | |
end | |
def eof? | |
@eof | |
end | |
def reset_variables(prompt = '', encoding:) | |
@prompt = prompt | |
@mark_pointer = nil | |
@encoding = encoding | |
@is_multiline = false | |
@finished = false | |
@cleared = false | |
@rerender_all = false | |
@history_pointer = nil | |
@kill_ring = Reline::KillRing.new | |
@vi_clipboard = '' | |
@vi_arg = nil | |
@waiting_proc = nil | |
@waiting_operator_proc = nil | |
@waiting_operator_vi_arg = nil | |
@completion_journey_data = nil | |
@completion_state = CompletionState::NORMAL | |
@perfect_matched = nil | |
@menu_info = nil | |
@first_prompt = true | |
@searching_prompt = nil | |
@first_char = true | |
@add_newline_to_end_of_buffer = false | |
@just_cursor_moving = nil | |
@cached_prompt_list = nil | |
@prompt_cache_time = nil | |
@eof = false | |
@continuous_insertion_buffer = String.new(encoding: @encoding) | |
reset_line | |
end | |
def reset_line | |
@cursor = 0 | |
@cursor_max = 0 | |
@byte_pointer = 0 | |
@buffer_of_lines = [String.new(encoding: @encoding)] | |
@line_index = 0 | |
@previous_line_index = nil | |
@line = @buffer_of_lines[0] | |
@first_line_started_from = 0 | |
@move_up = 0 | |
@started_from = 0 | |
@highest_in_this = 1 | |
@highest_in_all = 1 | |
@line_backup_in_history = nil | |
@multibyte_buffer = String.new(encoding: 'ASCII-8BIT') | |
@check_new_auto_indent = false | |
end | |
def multiline_on | |
@is_multiline = true | |
end | |
def multiline_off | |
@is_multiline = false | |
end | |
private def calculate_height_by_lines(lines, prompt) | |
result = 0 | |
prompt_list = prompt.is_a?(Array) ? prompt : nil | |
lines.each_with_index { |line, i| | |
prompt = prompt_list[i] if prompt_list and prompt_list[i] | |
result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line)) | |
} | |
result | |
end | |
private def insert_new_line(cursor_line, next_line) | |
@line = cursor_line | |
@buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding)) | |
@previous_line_index = @line_index | |
@line_index += 1 | |
@just_cursor_moving = false | |
end | |
private def calculate_height_by_width(width) | |
width.div(@screen_size.last) + 1 | |
end | |
private def split_by_width(str, max_width) | |
Reline::Unicode.split_by_width(str, max_width, @encoding) | |
end | |
private def scroll_down(val) | |
if val <= @rest_height | |
Reline::IOGate.move_cursor_down(val) | |
@rest_height -= val | |
else | |
Reline::IOGate.move_cursor_down(@rest_height) | |
Reline::IOGate.scroll_down(val - @rest_height) | |
@rest_height = 0 | |
end | |
end | |
private def move_cursor_up(val) | |
if val > 0 | |
Reline::IOGate.move_cursor_up(val) | |
@rest_height += val | |
elsif val < 0 | |
move_cursor_down(-val) | |
end | |
end | |
private def move_cursor_down(val) | |
if val > 0 | |
Reline::IOGate.move_cursor_down(val) | |
@rest_height -= val | |
@rest_height = 0 if @rest_height < 0 | |
elsif val < 0 | |
move_cursor_up(-val) | |
end | |
end | |
private def calculate_nearest_cursor | |
@cursor_max = calculate_width(line) | |
new_cursor = 0 | |
new_byte_pointer = 0 | |
height = 1 | |
max_width = @screen_size.last | |
if @config.editing_mode_is?(:vi_command) | |
last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize) | |
if last_byte_size > 0 | |
last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size) | |
last_width = Reline::Unicode.get_mbchar_width(last_mbchar) | |
cursor_max = @cursor_max - last_width | |
else | |
cursor_max = @cursor_max | |
end | |
else | |
cursor_max = @cursor_max | |
end | |
@line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc| | |
mbchar_width = Reline::Unicode.get_mbchar_width(gc) | |
now = new_cursor + mbchar_width | |
if now > cursor_max or now > @cursor | |
break | |
end | |
new_cursor += mbchar_width | |
if new_cursor > max_width * height | |
height += 1 | |
end | |
new_byte_pointer += gc.bytesize | |
end | |
@started_from = height - 1 | |
@cursor = new_cursor | |
@byte_pointer = new_byte_pointer | |
end | |
def rerender_all | |
@rerender_all = true | |
process_insert(force: true) | |
rerender | |
end | |
def rerender | |
return if @line.nil? | |
if @menu_info | |
scroll_down(@highest_in_all - @first_line_started_from) | |
@rerender_all = true | |
end | |
if @menu_info | |
show_menu | |
@menu_info = nil | |
end | |
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) | |
if @cleared | |
clear_screen_buffer(prompt, prompt_list, prompt_width) | |
@cleared = false | |
return | |
end | |
new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) | |
# FIXME: end of logical line sometimes breaks | |
if @add_newline_to_end_of_buffer | |
rerender_added_newline | |
@add_newline_to_end_of_buffer = false | |
elsif @just_cursor_moving and not @rerender_all | |
just_move_cursor | |
@just_cursor_moving = false | |
return | |
elsif @previous_line_index or new_highest_in_this != @highest_in_this | |
rerender_changed_current_line | |
@previous_line_index = nil | |
rendered = true | |
elsif @rerender_all | |
rerender_all_lines | |
@rerender_all = false | |
rendered = true | |
end | |
line = modify_lines(whole_lines)[@line_index] | |
if @is_multiline | |
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) | |
if finished? | |
# Always rerender on finish because output_modifier_proc may return a different output. | |
render_partial(prompt, prompt_width, line) | |
scroll_down(1) | |
Reline::IOGate.move_cursor_column(0) | |
Reline::IOGate.erase_after_cursor | |
elsif not rendered | |
# TODO ここを通っているっぽい | |
render_partial(prompt, prompt_width, line) | |
end | |
else | |
render_partial(prompt, prompt_width, line) | |
if finished? | |
scroll_down(1) | |
Reline::IOGate.move_cursor_column(0) | |
Reline::IOGate.erase_after_cursor | |
end | |
end | |
end | |
private def rerender_added_newline # 高速化のために | |
scroll_down(1) | |
new_lines = whole_lines(index: @previous_line_index, line: @line) | |
prompt, prompt_width, = check_multiline_prompt(new_lines, prompt) | |
@buffer_of_lines[@previous_line_index] = @line | |
@line = @buffer_of_lines[@line_index] | |
render_partial(prompt, prompt_width, @line, false) | |
@cursor = @cursor_max = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
@highest_in_all += @highest_in_this | |
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) | |
@first_line_started_from += @started_from + 1 | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
@previous_line_index = nil | |
end | |
def just_move_cursor # カーソル移動だけのときはレンダリングし直さない | |
prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt) | |
move_cursor_up(@started_from) | |
new_first_line_started_from = | |
if @line_index.zero? | |
0 | |
else | |
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) | |
end | |
@line = @buffer_of_lines[@line_index] | |
move_cursor_down(new_first_line_started_from - @first_line_started_from) | |
@first_line_started_from = new_first_line_started_from | |
calculate_nearest_cursor | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
move_cursor_down(@started_from) | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
@previous_line_index = nil | |
end | |
private def rerender_changed_current_line | |
if @previous_line_index | |
new_lines = whole_lines(index: @previous_line_index, line: @line) | |
else | |
new_lines = whole_lines | |
end | |
prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt) | |
all_height = calculate_height_by_lines(new_lines, prompt_list || prompt) | |
diff = all_height - @highest_in_all | |
move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1) | |
if diff > 0 | |
scroll_down(diff) | |
move_cursor_up(all_height - 1) | |
elsif diff < 0 | |
(-diff).times do | |
Reline::IOGate.move_cursor_column(0) | |
Reline::IOGate.erase_after_cursor | |
move_cursor_up(1) | |
end | |
move_cursor_up(all_height - 1) | |
else | |
move_cursor_up(all_height - 1) | |
end | |
@highest_in_all = all_height | |
back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width) | |
move_cursor_up(back) | |
if @previous_line_index | |
@buffer_of_lines[@previous_line_index] = @line | |
@line = @buffer_of_lines[@line_index] | |
end | |
@first_line_started_from = | |
if @line_index.zero? | |
0 | |
else | |
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) | |
end | |
if @prompt_proc | |
prompt = prompt_list[@line_index] | |
prompt_width = calculate_width(prompt, true) | |
end | |
move_cursor_down(@first_line_started_from) | |
calculate_nearest_cursor | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
move_cursor_down(@started_from) | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) | |
end | |
private def rerender_all_lines | |
move_cursor_up(@first_line_started_from + @started_from) | |
Reline::IOGate.move_cursor_column(0) | |
back = 0 | |
new_buffer = whole_lines | |
prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt) | |
new_buffer.each_with_index do |line, index| | |
prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc | |
width = prompt_width + calculate_width(line) | |
height = calculate_height_by_width(width) | |
back += height | |
end | |
if back > @highest_in_all | |
scroll_down(back - 1) | |
move_cursor_up(back - 1) | |
elsif back < @highest_in_all | |
scroll_down(back) | |
Reline::IOGate.erase_after_cursor | |
(@highest_in_all - back - 1).times do | |
scroll_down(1) | |
Reline::IOGate.erase_after_cursor | |
end | |
move_cursor_up(@highest_in_all - 1) | |
end | |
render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) | |
move_cursor_up(back - 1) | |
if @prompt_proc | |
prompt = prompt_list[@line_index] | |
prompt_width = calculate_width(prompt, true) | |
end | |
@highest_in_all = back | |
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) | |
@first_line_started_from = | |
if @line_index.zero? | |
0 | |
else | |
calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) | |
end | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
move_cursor_down(@first_line_started_from + @started_from) | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
end | |
private def render_whole_lines(lines, prompt, prompt_width) | |
rendered_height = 0 | |
modify_lines(lines).each_with_index do |line, index| | |
if prompt.is_a?(Array) | |
line_prompt = prompt[index] | |
prompt_width = calculate_width(line_prompt, true) | |
else | |
line_prompt = prompt | |
end | |
height = render_partial(line_prompt, prompt_width, line, false) | |
if index < (lines.size - 1) | |
scroll_down(1) | |
rendered_height += height | |
else | |
rendered_height += height - 1 | |
end | |
end | |
rendered_height | |
end | |
private def render_partial(prompt, prompt_width, line_to_render, with_control = true) | |
visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last) | |
if with_control | |
if height > @highest_in_this | |
diff = height - @highest_in_this | |
scroll_down(diff) | |
@highest_in_all += diff | |
@highest_in_this = height | |
move_cursor_up(diff) | |
elsif height < @highest_in_this | |
diff = @highest_in_this - height | |
@highest_in_all -= diff | |
@highest_in_this = height | |
end | |
move_cursor_up(@started_from) | |
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 | |
end | |
Reline::IOGate.move_cursor_column(0) | |
visual_lines.each_with_index do |line, index| | |
if line.nil? | |
if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last | |
# reaches the end of line | |
if Reline::IOGate.win? | |
# A newline is automatically inserted if a character is rendered at | |
# eol on command prompt. | |
else | |
# When the cursor is at the end of the line and erases characters | |
# after the cursor, some terminals delete the character at the | |
# cursor position. | |
move_cursor_down(1) | |
Reline::IOGate.move_cursor_column(0) | |
end | |
else | |
Reline::IOGate.erase_after_cursor | |
move_cursor_down(1) | |
Reline::IOGate.move_cursor_column(0) | |
end | |
next | |
end | |
@output.write line | |
if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last | |
# A newline is automatically inserted if a character is rendered at eol on command prompt. | |
@rest_height -= 1 if @rest_height > 0 | |
end | |
@output.flush | |
if @first_prompt | |
@first_prompt = false | |
@pre_input_hook&.call | |
end | |
end | |
Reline::IOGate.erase_after_cursor | |
Reline::IOGate.move_cursor_column(0) | |
if with_control | |
# Just after rendring, so the cursor is on the last line. | |
if finished? | |
Reline::IOGate.move_cursor_column(0) | |
else | |
# Moves up from bottom of lines to the cursor position. | |
move_cursor_up(height - 1 - @started_from) | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
end | |
end | |
height | |
end | |
private def modify_lines(before) | |
return before if before.nil? || before.empty? || simplified_rendering? | |
if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?) | |
after.lines("\n").map { |l| l.chomp('') } | |
else | |
before | |
end | |
end | |
private def show_menu | |
scroll_down(@highest_in_all - @first_line_started_from) | |
@rerender_all = true | |
@menu_info.list.sort!.each do |item| | |
Reline::IOGate.move_cursor_column(0) | |
@output.write item | |
@output.flush | |
scroll_down(1) | |
end | |
scroll_down(@highest_in_all - 1) | |
move_cursor_up(@highest_in_all - 1 - @first_line_started_from) | |
end | |
private def clear_screen_buffer(prompt, prompt_list, prompt_width) | |
Reline::IOGate.clear_screen | |
back = 0 | |
modify_lines(whole_lines).each_with_index do |line, index| | |
if @prompt_proc | |
pr = prompt_list[index] | |
height = render_partial(pr, calculate_width(pr), line, false) | |
else | |
height = render_partial(prompt, prompt_width, line, false) | |
end | |
if index < (@buffer_of_lines.size - 1) | |
move_cursor_down(height) | |
back += height | |
end | |
end | |
move_cursor_up(back) | |
move_cursor_down(@first_line_started_from + @started_from) | |
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) | |
end | |
def editing_mode | |
@config.editing_mode | |
end | |
private def menu(target, list) | |
@menu_info = MenuInfo.new(target, list) | |
end | |
private def complete_internal_proc(list, is_menu) | |
preposing, target, postposing = retrieve_completion_block | |
list = list.select { |i| | |
if i and not Encoding.compatible?(target.encoding, i.encoding) | |
raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}" | |
end | |
if @config.completion_ignore_case | |
i&.downcase&.start_with?(target.downcase) | |
else | |
i&.start_with?(target) | |
end | |
}.uniq | |
if is_menu | |
menu(target, list) | |
return nil | |
end | |
completed = list.inject { |memo, item| | |
begin | |
memo_mbchars = memo.unicode_normalize.grapheme_clusters | |
item_mbchars = item.unicode_normalize.grapheme_clusters | |
rescue Encoding::CompatibilityError | |
memo_mbchars = memo.grapheme_clusters | |
item_mbchars = item.grapheme_clusters | |
end | |
size = [memo_mbchars.size, item_mbchars.size].min | |
result = '' | |
size.times do |i| | |
if @config.completion_ignore_case | |
if memo_mbchars[i].casecmp?(item_mbchars[i]) | |
result << memo_mbchars[i] | |
else | |
break | |
end | |
else | |
if memo_mbchars[i] == item_mbchars[i] | |
result << memo_mbchars[i] | |
else | |
break | |
end | |
end | |
end | |
result | |
} | |
[target, preposing, completed, postposing] | |
end | |
private def complete(list, just_show_list = false) | |
case @completion_state | |
when CompletionState::NORMAL, CompletionState::JOURNEY | |
@completion_state = CompletionState::COMPLETION | |
when CompletionState::PERFECT_MATCH | |
@dig_perfect_match_proc&.(@perfect_matched) | |
end | |
if just_show_list | |
is_menu = true | |
elsif @completion_state == CompletionState::MENU | |
is_menu = true | |
elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH | |
is_menu = true | |
else | |
is_menu = false | |
end | |
result = complete_internal_proc(list, is_menu) | |
if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH | |
@completion_state = CompletionState::PERFECT_MATCH | |
end | |
return if result.nil? | |
target, preposing, completed, postposing = result | |
return if completed.nil? | |
if target <= completed and (@completion_state == CompletionState::COMPLETION) | |
if list.include?(completed) | |
if list.one? | |
@completion_state = CompletionState::PERFECT_MATCH | |
else | |
@completion_state = CompletionState::MENU_WITH_PERFECT_MATCH | |
end | |
@perfect_matched = completed | |
else | |
@completion_state = CompletionState::MENU | |
end | |
if not just_show_list and target < completed | |
@line = preposing + completed + completion_append_character.to_s + postposing | |
line_to_pointer = preposing + completed + completion_append_character.to_s | |
@cursor_max = calculate_width(@line) | |
@cursor = calculate_width(line_to_pointer) | |
@byte_pointer = line_to_pointer.bytesize | |
end | |
end | |
end | |
private def move_completed_list(list, direction) | |
case @completion_state | |
when CompletionState::NORMAL, CompletionState::COMPLETION, | |
CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH | |
@completion_state = CompletionState::JOURNEY | |
result = retrieve_completion_block | |
return if result.nil? | |
preposing, target, postposing = result | |
@completion_journey_data = CompletionJourneyData.new( | |
preposing, postposing, | |
[target] + list.select{ |item| item.start_with?(target) }, 0) | |
@completion_state = CompletionState::JOURNEY | |
else | |
case direction | |
when :up | |
@completion_journey_data.pointer -= 1 | |
if @completion_journey_data.pointer < 0 | |
@completion_journey_data.pointer = @completion_journey_data.list.size - 1 | |
end | |
when :down | |
@completion_journey_data.pointer += 1 | |
if @completion_journey_data.pointer >= @completion_journey_data.list.size | |
@completion_journey_data.pointer = 0 | |
end | |
end | |
completed = @completion_journey_data.list[@completion_journey_data.pointer] | |
@line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing | |
line_to_pointer = @completion_journey_data.preposing + completed | |
@cursor_max = calculate_width(@line) | |
@cursor = calculate_width(line_to_pointer) | |
@byte_pointer = line_to_pointer.bytesize | |
end | |
end | |
private def run_for_operators(key, method_symbol, &block) | |
if @waiting_operator_proc | |
if VI_MOTIONS.include?(method_symbol) | |
old_cursor, old_byte_pointer = @cursor, @byte_pointer | |
@vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1 | |
block.(true) | |
unless @waiting_proc | |
cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer | |
@cursor, @byte_pointer = old_cursor, old_byte_pointer | |
@waiting_operator_proc.(cursor_diff, byte_pointer_diff) | |
else | |
old_waiting_proc = @waiting_proc | |
old_waiting_operator_proc = @waiting_operator_proc | |
current_waiting_operator_proc = @waiting_operator_proc | |
@waiting_proc = proc { |k| | |
old_cursor, old_byte_pointer = @cursor, @byte_pointer | |
old_waiting_proc.(k) | |
cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer | |
@cursor, @byte_pointer = old_cursor, old_byte_pointer | |
current_waiting_operator_proc.(cursor_diff, byte_pointer_diff) | |
@waiting_operator_proc = old_waiting_operator_proc | |
} | |
end | |
else | |
# Ignores operator when not motion is given. | |
block.(false) | |
end | |
@waiting_operator_proc = nil | |
@waiting_operator_vi_arg = nil | |
@vi_arg = nil | |
else | |
block.(false) | |
end | |
end | |
private def argumentable?(method_obj) | |
method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg } | |
end | |
private def inclusive?(method_obj) | |
# If a motion method with the keyword argument "inclusive" follows the | |
# operator, it must contain the character at the cursor position. | |
method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive } | |
end | |
def wrap_method_call(method_symbol, method_obj, key, with_operator = false) | |
if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil? | |
not_insertion = method_symbol != :ed_insert | |
process_insert(force: not_insertion) | |
end | |
if @vi_arg and argumentable?(method_obj) | |
if with_operator and inclusive?(method_obj) | |
method_obj.(key, arg: @vi_arg, inclusive: true) | |
else | |
method_obj.(key, arg: @vi_arg) | |
end | |
else | |
if with_operator and inclusive?(method_obj) | |
method_obj.(key, inclusive: true) | |
else | |
method_obj.(key) | |
end | |
end | |
end | |
private def process_key(key, method_symbol) | |
if method_symbol and respond_to?(method_symbol, true) | |
method_obj = method(method_symbol) | |
else | |
method_obj = nil | |
end | |
if method_symbol and key.is_a?(Symbol) # メソッドが呼び出せたら | |
if @vi_arg and argumentable?(method_obj) | |
run_for_operators(key, method_symbol) do |with_operator| | |
wrap_method_call(method_symbol, method_obj, key, with_operator) # 呼ぶ | |
end | |
else | |
wrap_method_call(method_symbol, method_obj, key) if method_obj | |
end | |
@kill_ring.process | |
@vi_arg = nil | |
elsif @vi_arg | |
if key.chr =~ /[0-9]/ | |
ed_argument_digit(key) | |
else | |
if argumentable?(method_obj) | |
run_for_operators(key, method_symbol) do |with_operator| | |
wrap_method_call(method_symbol, method_obj, key, with_operator) | |
end | |
elsif @waiting_proc | |
@waiting_proc.(key) | |
elsif method_obj | |
wrap_method_call(method_symbol, method_obj, key) | |
else | |
ed_insert(key) unless @config.editing_mode_is?(:vi_command) | |
end | |
@kill_ring.process | |
@vi_arg = nil | |
end | |
elsif @waiting_proc | |
@waiting_proc.(key) | |
@kill_ring.process | |
elsif method_obj | |
if method_symbol == :ed_argument_digit | |
wrap_method_call(method_symbol, method_obj, key) | |
else | |
run_for_operators(key, method_symbol) do |with_operator| | |
wrap_method_call(method_symbol, method_obj, key, with_operator) | |
end | |
end | |
@kill_ring.process # emacsのkill_ring. yankしたもの(kill)をぐるぐる入れる | |
else | |
ed_insert(key) unless @config.editing_mode_is?(:vi_command) # マクロが呼び出せなければ ed(it)_insert | |
end | |
end | |
private def normal_char(key) | |
method_symbol = method_obj = nil | |
if key.combined_char.is_a?(Symbol) | |
process_key(key.combined_char, key.combined_char) | |
return | |
end | |
@multibyte_buffer << key.combined_char | |
# key は原則1バイトずつ | |
if @multibyte_buffer.size > 1 | |
if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding? | |
# encodeする => 特定のフォーマットへの変換 | |
# encoding はフォーマットそのもののこと. force_encoding はフォーマットの状態を変更するだけで変更しない. 拡張子変えるだけみたいな | |
# そのエンコーディングでただしいかの確認. valid_encodingは. 文字化けしてないかの確認みたいなもの | |
# TODO: memo | |
process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil) | |
@multibyte_buffer.clear | |
else | |
# invalid | |
return # マルチバイトの途中のバイト列だとskip | |
end | |
else # single byte | |
return if key.char >= 128 # maybe, first byte of multi byte | |
# マルチバイト文字の途中でなければ128以上にならないように端末が書き換える | |
method_symbol = @config.editing_mode.get_method(key.combined_char) # マクロを抽出 | |
if key.with_meta and method_symbol == :ed_unassigned | |
# split ESC + key | |
method_symbol = @config.editing_mode.get_method("\e".ord) | |
process_key("\e".ord, method_symbol) | |
method_symbol = @config.editing_mode.get_method(key.char) | |
process_key(key.char, method_symbol) | |
else | |
process_key(key.combined_char, method_symbol) | |
end | |
@multibyte_buffer.clear | |
end | |
if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max | |
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
mbchar = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor -= width | |
end | |
end | |
def input_key(key) | |
@just_cursor_moving = nil | |
if key.char.nil? | |
if @first_char | |
@line = nil | |
end | |
finish | |
return | |
end | |
@first_char = false | |
completion_occurs = false | |
if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord | |
unless @config.disable_completion | |
result = call_completion_proc | |
if result.is_a?(Array) | |
completion_occurs = true | |
process_insert | |
complete(result) | |
end | |
end | |
elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) | |
unless @config.disable_completion | |
result = call_completion_proc | |
if result.is_a?(Array) | |
completion_occurs = true | |
process_insert | |
move_completed_list(result, "\C-p".ord == key.char ? :up : :down) | |
end | |
end | |
elsif Symbol === key.char and respond_to?(key.char, true) | |
process_key(key.char, key.char) | |
else | |
# TODO ここ | |
normal_char(key) | |
end | |
unless completion_occurs | |
@completion_state = CompletionState::NORMAL | |
end | |
if not Reline::IOGate.in_pasting? and @just_cursor_moving.nil? | |
if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line | |
@just_cursor_moving = true | |
elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line | |
@just_cursor_moving = true | |
else | |
@just_cursor_moving = false | |
end | |
else | |
@just_cursor_moving = false | |
end | |
if @is_multiline and @auto_indent_proc and not simplified_rendering? | |
process_auto_indent | |
end | |
end | |
def call_completion_proc | |
result = retrieve_completion_block(true) | |
slice = result[1] | |
result = @completion_proc.(slice) if @completion_proc and slice | |
Reline.core.instance_variable_set(:@completion_quote_character, nil) | |
result | |
end | |
private def process_auto_indent | |
return if not @check_new_auto_indent and @previous_line_index # move cursor up or down | |
if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index | |
# Fix indent of a line when a newline is inserted to the next | |
new_lines = whole_lines(index: @previous_line_index, line: @line) | |
new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true) | |
md = @line.match(/\A */) | |
prev_indent = md[0].count(' ') | |
@line = ' ' * new_indent + @line.lstrip | |
new_indent = nil | |
result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false) | |
if result | |
new_indent = result | |
end | |
if new_indent&.>= 0 | |
@line = ' ' * new_indent + @line.lstrip | |
end | |
end | |
if @previous_line_index | |
new_lines = whole_lines(index: @previous_line_index, line: @line) | |
else | |
new_lines = whole_lines | |
end | |
new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent) | |
if new_indent&.>= 0 | |
md = new_lines[@line_index].match(/\A */) | |
prev_indent = md[0].count(' ') | |
if @check_new_auto_indent | |
@buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip | |
@cursor = new_indent | |
@byte_pointer = new_indent | |
else | |
@line = ' ' * new_indent + @line.lstrip | |
@cursor += new_indent - prev_indent | |
@byte_pointer += new_indent - prev_indent | |
end | |
end | |
@check_new_auto_indent = false | |
end | |
def retrieve_completion_block(set_completion_quote_character = false) | |
word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ | |
quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ | |
before = @line.byteslice(0, @byte_pointer) | |
rest = nil | |
break_pointer = nil | |
quote = nil | |
closing_quote = nil | |
escaped_quote = nil | |
i = 0 | |
while i < @byte_pointer do | |
slice = @line.byteslice(i, @byte_pointer - i) | |
unless slice.valid_encoding? | |
i += 1 | |
next | |
end | |
if quote and slice.start_with?(closing_quote) | |
quote = nil | |
i += 1 | |
rest = nil | |
elsif quote and slice.start_with?(escaped_quote) | |
# skip | |
i += 2 | |
elsif 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 not quote and slice =~ word_break_regexp | |
rest = $' | |
i += 1 | |
before = @line.byteslice(i, @byte_pointer - i) | |
break_pointer = i | |
else | |
i += 1 | |
end | |
end | |
postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) | |
if rest | |
preposing = @line.byteslice(0, break_pointer) | |
target = rest | |
if set_completion_quote_character and quote | |
Reline.core.instance_variable_set(:@completion_quote_character, quote) | |
if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote | |
insert_text(quote) | |
end | |
end | |
else | |
preposing = '' | |
if break_pointer | |
preposing = @line.byteslice(0, break_pointer) | |
else | |
preposing = '' | |
end | |
target = before | |
end | |
[preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)] | |
end | |
def confirm_multiline_termination | |
temp_buffer = @buffer_of_lines.dup | |
if @previous_line_index and @line_index == (@buffer_of_lines.size - 1) | |
temp_buffer[@previous_line_index] = @line | |
else | |
temp_buffer[@line_index] = @line | |
end | |
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") | |
end | |
def insert_text(text) | |
width = calculate_width(text) | |
if @cursor == @cursor_max | |
@line += text | |
else | |
@line = byteinsert(@line, @byte_pointer, text) | |
end | |
@byte_pointer += text.bytesize | |
@cursor += width | |
@cursor_max += width | |
end | |
def delete_text(start = nil, length = nil) | |
if start.nil? and length.nil? | |
@line&.clear | |
@byte_pointer = 0 | |
@cursor = 0 | |
@cursor_max = 0 | |
elsif not start.nil? and not length.nil? | |
if @line | |
before = @line.byteslice(0, start) | |
after = @line.byteslice(start + length, @line.bytesize) | |
@line = before + after | |
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize | |
str = @line.byteslice(0, @byte_pointer) | |
@cursor = calculate_width(str) | |
@cursor_max = calculate_width(@line) | |
end | |
elsif start.is_a?(Range) | |
range = start | |
first = range.first | |
last = range.last | |
last = @line.bytesize - 1 if last > @line.bytesize | |
last += @line.bytesize if last < 0 | |
first += @line.bytesize if first < 0 | |
range = range.exclude_end? ? first...last : first..last | |
@line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding) | |
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize | |
str = @line.byteslice(0, @byte_pointer) | |
@cursor = calculate_width(str) | |
@cursor_max = calculate_width(@line) | |
else | |
@line = @line.byteslice(0, start) | |
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize | |
str = @line.byteslice(0, @byte_pointer) | |
@cursor = calculate_width(str) | |
@cursor_max = calculate_width(@line) | |
end | |
end | |
def byte_pointer=(val) | |
@byte_pointer = val | |
str = @line.byteslice(0, @byte_pointer) | |
@cursor = calculate_width(str) | |
@cursor_max = calculate_width(@line) | |
end | |
def whole_lines(index: @line_index, line: @line) | |
temp_lines = @buffer_of_lines.dup | |
temp_lines[index] = line | |
temp_lines | |
end | |
def whole_buffer | |
if @buffer_of_lines.size == 1 and @line.nil? | |
nil | |
else | |
whole_lines.join("\n") | |
end | |
end | |
def finished? | |
@finished | |
end | |
def finish | |
@finished = true | |
@rerender_all = true | |
@config.reset | |
end | |
private def byteslice!(str, byte_pointer, size) | |
new_str = str.byteslice(0, byte_pointer) | |
new_str << str.byteslice(byte_pointer + size, str.bytesize) | |
[new_str, str.byteslice(byte_pointer, size)] | |
end | |
private def byteinsert(str, byte_pointer, other) | |
new_str = str.byteslice(0, byte_pointer) | |
new_str << other | |
new_str << str.byteslice(byte_pointer, str.bytesize) | |
new_str | |
end | |
private def calculate_width(str, allow_escape_code = false) | |
Reline::Unicode.calculate_width(str, allow_escape_code) | |
end | |
private def key_delete(key) | |
if @config.editing_mode_is?(:vi_insert, :emacs) | |
ed_delete_next_char(key) | |
end | |
end | |
private def key_newline(key) | |
if @is_multiline | |
if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer | |
@add_newline_to_end_of_buffer = true | |
end | |
next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) | |
cursor_line = @line.byteslice(0, @byte_pointer) | |
insert_new_line(cursor_line, next_line) | |
@cursor = 0 | |
@check_new_auto_indent = true | |
end | |
end | |
private def ed_unassigned(key) end # do nothing | |
private def process_insert(force: false) | |
# TODO: memo | |
return if @continuous_insertion_buffer.empty? or (Reline::IOGate.in_pasting? and not force) | |
width = Reline::Unicode.calculate_width(@continuous_insertion_buffer) | |
bytesize = @continuous_insertion_buffer.bytesize | |
if @cursor == @cursor_max | |
@line += @continuous_insertion_buffer | |
else | |
@line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer) | |
end | |
@byte_pointer += bytesize | |
@cursor += width | |
@cursor_max += width | |
@continuous_insertion_buffer.clear | |
end | |
private def ed_insert(key) | |
str = nil | |
width = nil | |
bytesize = nil | |
if key.instance_of?(String) # 壊れたデータでないかチェック | |
begin | |
key.encode(Encoding::UTF_8) | |
rescue Encoding::UndefinedConversionError | |
return | |
end | |
str = key | |
bytesize = key.bytesize | |
else | |
begin | |
key.chr.encode(Encoding::UTF_8) | |
rescue Encoding::UndefinedConversionError | |
return | |
end | |
str = key.chr # 入力された文字が入る | |
bytesize = 1 | |
end | |
if Reline::IOGate.in_pasting? | |
@continuous_insertion_buffer << str | |
return | |
elsif not @continuous_insertion_buffer.empty? | |
process_insert | |
end | |
width = Reline::Unicode.get_mbchar_width(str) | |
if @cursor == @cursor_max # @cursor_max は行末 | |
@line += str | |
else | |
@line = byteinsert(@line, @byte_pointer, str) # 行末でなければ | |
# @line現在行 | |
# 挿入位置より前で結合できるか確認 | |
# get_prev_mbchar_size で現在のstrと前の文字が結合できるか | |
# bytesliceで bytepointer-1文字前の書記素のbytesize から bytepointerまで + str => grapheme_clustersが増えるか? | |
# 増えてなければ1書記素. width = 0 | |
end | |
@byte_pointer += bytesize # 文字列上のバイト位置 | |
# あいう # いは byte_pointer が3 | |
# abcd # b は byte_pointer が1 | |
@cursor += width | |
@cursor_max += width | |
end | |
alias_method :ed_digit, :ed_insert | |
alias_method :self_insert, :ed_insert | |
private def ed_quoted_insert(str, arg: 1) | |
@waiting_proc = proc { |key| | |
arg.times do | |
if key == "\C-j".ord or key == "\C-m".ord | |
key_newline(key) | |
else | |
ed_insert(key) | |
end | |
end | |
@waiting_proc = nil | |
} | |
end | |
alias_method :quoted_insert, :ed_quoted_insert | |
private def ed_next_char(key, arg: 1) | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
if (@byte_pointer < @line.bytesize) | |
mbchar = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor += width if width | |
@byte_pointer += byte_size | |
elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1 | |
next_line = @buffer_of_lines[@line_index + 1] | |
@cursor = 0 | |
@byte_pointer = 0 | |
@cursor_max = calculate_width(next_line) | |
@previous_line_index = @line_index | |
@line_index += 1 | |
end | |
arg -= 1 | |
ed_next_char(key, arg: arg) if arg > 0 | |
end | |
alias_method :forward_char, :ed_next_char | |
private def ed_prev_char(key, arg: 1) | |
if @cursor > 0 | |
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
mbchar = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor -= width | |
elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0 | |
prev_line = @buffer_of_lines[@line_index - 1] | |
@cursor = calculate_width(prev_line) | |
@byte_pointer = prev_line.bytesize | |
@cursor_max = calculate_width(prev_line) | |
@previous_line_index = @line_index | |
@line_index -= 1 | |
end | |
arg -= 1 | |
ed_prev_char(key, arg: arg) if arg > 0 | |
end | |
alias_method :backward_char, :ed_prev_char | |
private def vi_first_print(key) | |
@byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line) | |
end | |
private def ed_move_to_beg(key) | |
@byte_pointer = @cursor = 0 | |
end | |
alias_method :beginning_of_line, :ed_move_to_beg | |
private def ed_move_to_end(key) | |
@byte_pointer = 0 | |
@cursor = 0 | |
byte_size = 0 | |
while @byte_pointer < @line.bytesize | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
if byte_size > 0 | |
mbchar = @line.byteslice(@byte_pointer, byte_size) | |
@cursor += Reline::Unicode.get_mbchar_width(mbchar) | |
end | |
@byte_pointer += byte_size | |
end | |
end | |
alias_method :end_of_line, :ed_move_to_end | |
private def generate_searcher | |
Fiber.new do |first_key| | |
prev_search_key = first_key | |
search_word = String.new(encoding: @encoding) | |
multibyte_buf = String.new(encoding: 'ASCII-8BIT') | |
last_hit = nil | |
case first_key | |
when "\C-r".ord | |
prompt_name = 'reverse-i-search' | |
when "\C-s".ord | |
prompt_name = 'i-search' | |
end | |
loop do | |
key = Fiber.yield(search_word) | |
search_again = false | |
case key | |
when -1 # determined | |
Reline.last_incremental_search = search_word | |
break | |
when "\C-h".ord, "\C-?".ord | |
grapheme_clusters = search_word.grapheme_clusters | |
if grapheme_clusters.size > 0 | |
grapheme_clusters.pop | |
search_word = grapheme_clusters.join | |
end | |
when "\C-r".ord, "\C-s".ord | |
search_again = true if prev_search_key == key | |
prev_search_key = key | |
else | |
multibyte_buf << key | |
if multibyte_buf.dup.force_encoding(@encoding).valid_encoding? | |
search_word << multibyte_buf.dup.force_encoding(@encoding) | |
multibyte_buf.clear | |
end | |
end | |
hit = nil | |
if not search_word.empty? and @line_backup_in_history&.include?(search_word) | |
@history_pointer = nil | |
hit = @line_backup_in_history | |
else | |
if search_again | |
if search_word.empty? and Reline.last_incremental_search | |
search_word = Reline.last_incremental_search | |
end | |
if @history_pointer | |
case prev_search_key | |
when "\C-r".ord | |
history_pointer_base = 0 | |
history = Reline::HISTORY[0..(@history_pointer - 1)] | |
when "\C-s".ord | |
history_pointer_base = @history_pointer + 1 | |
history = Reline::HISTORY[(@history_pointer + 1)..-1] | |
end | |
else | |
history_pointer_base = 0 | |
history = Reline::HISTORY | |
end | |
elsif @history_pointer | |
case prev_search_key | |
when "\C-r".ord | |
history_pointer_base = 0 | |
history = Reline::HISTORY[0..@history_pointer] | |
when "\C-s".ord | |
history_pointer_base = @history_pointer | |
history = Reline::HISTORY[@history_pointer..-1] | |
end | |
else | |
history_pointer_base = 0 | |
history = Reline::HISTORY | |
end | |
case prev_search_key | |
when "\C-r".ord | |
hit_index = history.rindex { |item| | |
item.include?(search_word) | |
} | |
when "\C-s".ord | |
hit_index = history.index { |item| | |
item.include?(search_word) | |
} | |
end | |
if hit_index | |
@history_pointer = history_pointer_base + hit_index | |
hit = Reline::HISTORY[@history_pointer] | |
end | |
end | |
case prev_search_key | |
when "\C-r".ord | |
prompt_name = 'reverse-i-search' | |
when "\C-s".ord | |
prompt_name = 'i-search' | |
end | |
if hit | |
if @is_multiline | |
@buffer_of_lines = hit.split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
@searching_prompt = "(%s)`%s'" % [prompt_name, search_word] | |
else | |
@line = hit | |
@searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit] | |
end | |
last_hit = hit | |
else | |
if @is_multiline | |
@rerender_all = true | |
@searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word] | |
else | |
@searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit] | |
end | |
end | |
end | |
end | |
end | |
private def incremental_search_history(key) | |
unless @history_pointer | |
if @is_multiline | |
@line_backup_in_history = whole_buffer | |
else | |
@line_backup_in_history = @line | |
end | |
end | |
searcher = generate_searcher | |
searcher.resume(key) | |
@searching_prompt = "(reverse-i-search)`': " | |
@waiting_proc = ->(k) { | |
case k | |
when "\C-j".ord | |
if @history_pointer | |
buffer = Reline::HISTORY[@history_pointer] | |
else | |
buffer = @line_backup_in_history | |
end | |
if @is_multiline | |
@buffer_of_lines = buffer.split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
@line = buffer | |
end | |
@searching_prompt = nil | |
@waiting_proc = nil | |
@cursor_max = calculate_width(@line) | |
@cursor = @byte_pointer = 0 | |
searcher.resume(-1) | |
when "\C-g".ord | |
if @is_multiline | |
@buffer_of_lines = @line_backup_in_history.split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
@line = @line_backup_in_history | |
end | |
@history_pointer = nil | |
@searching_prompt = nil | |
@waiting_proc = nil | |
@line_backup_in_history = nil | |
@cursor_max = calculate_width(@line) | |
@cursor = @byte_pointer = 0 | |
@rerender_all = true | |
else | |
chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT) | |
if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord | |
searcher.resume(k) | |
else | |
if @history_pointer | |
line = Reline::HISTORY[@history_pointer] | |
else | |
line = @line_backup_in_history | |
end | |
if @is_multiline | |
@line_backup_in_history = whole_buffer | |
@buffer_of_lines = line.split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
@line_backup_in_history = @line | |
@line = line | |
end | |
@searching_prompt = nil | |
@waiting_proc = nil | |
@cursor_max = calculate_width(@line) | |
@cursor = @byte_pointer = 0 | |
searcher.resume(-1) | |
end | |
end | |
} | |
end | |
private def vi_search_prev(key) | |
incremental_search_history(key) | |
end | |
alias_method :reverse_search_history, :vi_search_prev | |
private def vi_search_next(key) | |
incremental_search_history(key) | |
end | |
alias_method :forward_search_history, :vi_search_next | |
private def ed_search_prev_history(key, arg: 1) | |
history = nil | |
h_pointer = nil | |
line_no = nil | |
substr = @line.slice(0, @byte_pointer) | |
if @history_pointer.nil? | |
return if not @line.empty? and substr.empty? | |
history = Reline::HISTORY | |
elsif @history_pointer.zero? | |
history = nil | |
h_pointer = nil | |
else | |
history = Reline::HISTORY.slice(0, @history_pointer) | |
end | |
return if history.nil? | |
if @is_multiline | |
h_pointer = history.rindex { |h| | |
h.split("\n").each_with_index { |l, i| | |
if l.start_with?(substr) | |
line_no = i | |
break | |
end | |
} | |
not line_no.nil? | |
} | |
else | |
h_pointer = history.rindex { |l| | |
l.start_with?(substr) | |
} | |
end | |
return if h_pointer.nil? | |
@history_pointer = h_pointer | |
if @is_multiline | |
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = line_no | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
@line = Reline::HISTORY[@history_pointer] | |
end | |
@cursor_max = calculate_width(@line) | |
arg -= 1 | |
ed_search_prev_history(key, arg: arg) if arg > 0 | |
end | |
alias_method :history_search_backward, :ed_search_prev_history | |
private def ed_search_next_history(key, arg: 1) | |
substr = @line.slice(0, @byte_pointer) | |
if @history_pointer.nil? | |
return | |
elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty? | |
return | |
end | |
history = Reline::HISTORY.slice((@history_pointer + 1)..-1) | |
h_pointer = nil | |
line_no = nil | |
if @is_multiline | |
h_pointer = history.index { |h| | |
h.split("\n").each_with_index { |l, i| | |
if l.start_with?(substr) | |
line_no = i | |
break | |
end | |
} | |
not line_no.nil? | |
} | |
else | |
h_pointer = history.index { |l| | |
l.start_with?(substr) | |
} | |
end | |
h_pointer += @history_pointer + 1 if h_pointer and @history_pointer | |
return if h_pointer.nil? and not substr.empty? | |
@history_pointer = h_pointer | |
if @is_multiline | |
if @history_pointer.nil? and substr.empty? | |
@buffer_of_lines = [] | |
@line_index = 0 | |
else | |
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") | |
@line_index = line_no | |
end | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
if @history_pointer.nil? and substr.empty? | |
@line = '' | |
else | |
@line = Reline::HISTORY[@history_pointer] | |
end | |
end | |
@cursor_max = calculate_width(@line) | |
arg -= 1 | |
ed_search_next_history(key, arg: arg) if arg > 0 | |
end | |
alias_method :history_search_forward, :ed_search_next_history | |
private def ed_prev_history(key, arg: 1) | |
if @is_multiline and @line_index > 0 | |
@previous_line_index = @line_index | |
@line_index -= 1 | |
return | |
end | |
if Reline::HISTORY.empty? | |
return | |
end | |
if @history_pointer.nil? | |
@history_pointer = Reline::HISTORY.size - 1 | |
if @is_multiline | |
@line_backup_in_history = whole_buffer | |
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
@line_backup_in_history = @line | |
@line = Reline::HISTORY[@history_pointer] | |
end | |
elsif @history_pointer.zero? | |
return | |
else | |
if @is_multiline | |
Reline::HISTORY[@history_pointer] = whole_buffer | |
@history_pointer -= 1 | |
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = @buffer_of_lines.size - 1 | |
@line = @buffer_of_lines.last | |
@rerender_all = true | |
else | |
Reline::HISTORY[@history_pointer] = @line | |
@history_pointer -= 1 | |
@line = Reline::HISTORY[@history_pointer] | |
end | |
end | |
if @config.editing_mode_is?(:emacs, :vi_insert) | |
@cursor_max = @cursor = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
elsif @config.editing_mode_is?(:vi_command) | |
@byte_pointer = @cursor = 0 | |
@cursor_max = calculate_width(@line) | |
end | |
arg -= 1 | |
ed_prev_history(key, arg: arg) if arg > 0 | |
end | |
private def ed_next_history(key, arg: 1) | |
if @is_multiline and @line_index < (@buffer_of_lines.size - 1) | |
@previous_line_index = @line_index | |
@line_index += 1 | |
return | |
end | |
if @history_pointer.nil? | |
return | |
elsif @history_pointer == (Reline::HISTORY.size - 1) | |
if @is_multiline | |
@history_pointer = nil | |
@buffer_of_lines = @line_backup_in_history.split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = 0 | |
@line = @buffer_of_lines.first | |
@rerender_all = true | |
else | |
@history_pointer = nil | |
@line = @line_backup_in_history | |
end | |
else | |
if @is_multiline | |
Reline::HISTORY[@history_pointer] = whole_buffer | |
@history_pointer += 1 | |
@buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") | |
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? | |
@line_index = 0 | |
@line = @buffer_of_lines.first | |
@rerender_all = true | |
else | |
Reline::HISTORY[@history_pointer] = @line | |
@history_pointer += 1 | |
@line = Reline::HISTORY[@history_pointer] | |
end | |
end | |
@line = '' unless @line | |
if @config.editing_mode_is?(:emacs, :vi_insert) | |
@cursor_max = @cursor = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
elsif @config.editing_mode_is?(:vi_command) | |
@byte_pointer = @cursor = 0 | |
@cursor_max = calculate_width(@line) | |
end | |
arg -= 1 | |
ed_next_history(key, arg: arg) if arg > 0 | |
end | |
private def ed_newline(key) | |
process_insert(force: true) | |
if @is_multiline | |
if @config.editing_mode_is?(:vi_command) | |
if @line_index < (@buffer_of_lines.size - 1) | |
ed_next_history(key) # means cursor down | |
else | |
# should check confirm_multiline_termination to finish? | |
finish | |
end | |
else | |
if @line_index == (@buffer_of_lines.size - 1) | |
if confirm_multiline_termination | |
finish | |
else | |
key_newline(key) | |
end | |
else | |
# should check confirm_multiline_termination to finish? | |
@previous_line_index = @line_index | |
@line_index = @buffer_of_lines.size - 1 | |
finish | |
end | |
end | |
else | |
if @history_pointer | |
Reline::HISTORY[@history_pointer] = @line | |
@history_pointer = nil | |
end | |
finish | |
end | |
end | |
private def em_delete_prev_char(key) | |
if @is_multiline and @cursor == 0 and @line_index > 0 | |
@buffer_of_lines[@line_index] = @line | |
@cursor = calculate_width(@buffer_of_lines[@line_index - 1]) | |
@byte_pointer = @buffer_of_lines[@line_index - 1].bytesize | |
@buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) | |
@line_index -= 1 | |
@line = @buffer_of_lines[@line_index] | |
@cursor_max = calculate_width(@line) | |
@rerender_all = true | |
elsif @cursor > 0 | |
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor -= width | |
@cursor_max -= width | |
end | |
end | |
alias_method :backward_delete_char, :em_delete_prev_char | |
private def ed_kill_line(key) | |
if @line.bytesize > @byte_pointer | |
@line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer) | |
@byte_pointer = @line.bytesize | |
@cursor = @cursor_max = calculate_width(@line) | |
@kill_ring.append(deleted) | |
elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 | |
@cursor = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
@line += @buffer_of_lines.delete_at(@line_index + 1) | |
@cursor_max = calculate_width(@line) | |
@buffer_of_lines[@line_index] = @line | |
@rerender_all = true | |
@rest_height += 1 | |
end | |
end | |
private def em_kill_line(key) | |
if @byte_pointer > 0 | |
@line, deleted = byteslice!(@line, 0, @byte_pointer) | |
@byte_pointer = 0 | |
@kill_ring.append(deleted, true) | |
@cursor_max = calculate_width(@line) | |
@cursor = 0 | |
end | |
end | |
private def em_delete(key) | |
if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) | |
@line = nil | |
if @buffer_of_lines.size > 1 | |
scroll_down(@highest_in_all - @first_line_started_from) | |
end | |
Reline::IOGate.move_cursor_column(0) | |
@eof = true | |
finish | |
elsif @byte_pointer < @line.bytesize | |
splitted_last = @line.byteslice(@byte_pointer, @line.bytesize) | |
mbchar = splitted_last.grapheme_clusters.first | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor_max -= width | |
@line, = byteslice!(@line, @byte_pointer, mbchar.bytesize) | |
elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 | |
@cursor = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
@line += @buffer_of_lines.delete_at(@line_index + 1) | |
@cursor_max = calculate_width(@line) | |
@buffer_of_lines[@line_index] = @line | |
@rerender_all = true | |
@rest_height += 1 | |
end | |
end | |
alias_method :delete_char, :em_delete | |
private def em_delete_or_list(key) | |
if @line.empty? or @byte_pointer < @line.bytesize | |
em_delete(key) | |
else # show completed list | |
result = call_completion_proc | |
if result.is_a?(Array) | |
complete(result, true) | |
end | |
end | |
end | |
alias_method :delete_char_or_list, :em_delete_or_list | |
private def em_yank(key) | |
yanked = @kill_ring.yank | |
if yanked | |
@line = byteinsert(@line, @byte_pointer, yanked) | |
yanked_width = calculate_width(yanked) | |
@cursor += yanked_width | |
@cursor_max += yanked_width | |
@byte_pointer += yanked.bytesize | |
end | |
end | |
private def em_yank_pop(key) | |
yanked, prev_yank = @kill_ring.yank_pop | |
if yanked | |
prev_yank_width = calculate_width(prev_yank) | |
@cursor -= prev_yank_width | |
@cursor_max -= prev_yank_width | |
@byte_pointer -= prev_yank.bytesize | |
@line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize) | |
@line = byteinsert(@line, @byte_pointer, yanked) | |
yanked_width = calculate_width(yanked) | |
@cursor += yanked_width | |
@cursor_max += yanked_width | |
@byte_pointer += yanked.bytesize | |
end | |
end | |
private def ed_clear_screen(key) | |
@cleared = true | |
end | |
alias_method :clear_screen, :ed_clear_screen | |
private def em_next_word(key) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
end | |
alias_method :forward_word, :em_next_word | |
private def ed_prev_word(key) | |
if @byte_pointer > 0 | |
byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@cursor -= width | |
end | |
end | |
alias_method :backward_word, :ed_prev_word | |
private def em_delete_next_word(key) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) | |
@line, word = byteslice!(@line, @byte_pointer, byte_size) | |
@kill_ring.append(word) | |
@cursor_max -= width | |
end | |
end | |
private def ed_delete_prev_word(key) | |
if @byte_pointer > 0 | |
byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) | |
@line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size) | |
@kill_ring.append(word, true) | |
@byte_pointer -= byte_size | |
@cursor -= width | |
@cursor_max -= width | |
end | |
end | |
private def ed_transpose_chars(key) | |
if @byte_pointer > 0 | |
if @cursor_max > @cursor | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
mbchar = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor += width | |
@byte_pointer += byte_size | |
end | |
back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
if (@byte_pointer - back1_byte_size) > 0 | |
back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size) | |
back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size | |
@line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size) | |
@line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar) | |
end | |
end | |
end | |
alias_method :transpose_chars, :ed_transpose_chars | |
private def ed_transpose_words(key) | |
left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer) | |
before = @line.byteslice(0, left_word_start) | |
left_word = @line.byteslice(left_word_start, middle_start - left_word_start) | |
middle = @line.byteslice(middle_start, right_word_start - middle_start) | |
right_word = @line.byteslice(right_word_start, after_start - right_word_start) | |
after = @line.byteslice(after_start, @line.bytesize - after_start) | |
return if left_word.empty? or right_word.empty? | |
@line = before + right_word + middle + left_word + after | |
from_head_to_left_word = before + right_word + middle + left_word | |
@byte_pointer = from_head_to_left_word.bytesize | |
@cursor = calculate_width(from_head_to_left_word) | |
end | |
alias_method :transpose_words, :ed_transpose_words | |
private def em_capitol_case(key) | |
if @line.bytesize > @byte_pointer | |
byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer) | |
before = @line.byteslice(0, @byte_pointer) | |
after = @line.byteslice((@byte_pointer + byte_size)..-1) | |
@line = before + new_str + after | |
@byte_pointer += new_str.bytesize | |
@cursor += calculate_width(new_str) | |
end | |
end | |
alias_method :capitalize_word, :em_capitol_case | |
private def em_lower_case(key) | |
if @line.bytesize > @byte_pointer | |
byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) | |
part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| | |
mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar | |
}.join | |
rest = @line.byteslice((@byte_pointer + byte_size)..-1) | |
@line = @line.byteslice(0, @byte_pointer) + part | |
@byte_pointer = @line.bytesize | |
@cursor = calculate_width(@line) | |
@cursor_max = @cursor + calculate_width(rest) | |
@line += rest | |
end | |
end | |
alias_method :downcase_word, :em_lower_case | |
private def em_upper_case(key) | |
if @line.bytesize > @byte_pointer | |
byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) | |
part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| | |
mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar | |
}.join | |
rest = @line.byteslice((@byte_pointer + byte_size)..-1) | |
@line = @line.byteslice(0, @byte_pointer) + part | |
@byte_pointer = @line.bytesize | |
@cursor = calculate_width(@line) | |
@cursor_max = @cursor + calculate_width(rest) | |
@line += rest | |
end | |
end | |
alias_method :upcase_word, :em_upper_case | |
private def em_kill_region(key) | |
if @byte_pointer > 0 | |
byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer) | |
@line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size) | |
@byte_pointer -= byte_size | |
@cursor -= width | |
@cursor_max -= width | |
@kill_ring.append(deleted) | |
end | |
end | |
private def copy_for_vi(text) | |
if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command) | |
@vi_clipboard = text | |
end | |
end | |
private def vi_insert(key) | |
@config.editing_mode = :vi_insert | |
end | |
private def vi_add(key) | |
@config.editing_mode = :vi_insert | |
ed_next_char(key) | |
end | |
private def vi_command_mode(key) | |
ed_prev_char(key) | |
@config.editing_mode = :vi_command | |
end | |
alias_method :vi_movement_mode, :vi_command_mode | |
private def vi_next_word(key, arg: 1) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
arg -= 1 | |
vi_next_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_prev_word(key, arg: 1) | |
if @byte_pointer > 0 | |
byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@cursor -= width | |
end | |
arg -= 1 | |
vi_prev_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_end_word(key, arg: 1, inclusive: false) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
arg -= 1 | |
if inclusive and arg.zero? | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
if byte_size > 0 | |
c = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(c) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
end | |
vi_end_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_next_big_word(key, arg: 1) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
arg -= 1 | |
vi_next_big_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_prev_big_word(key, arg: 1) | |
if @byte_pointer > 0 | |
byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@cursor -= width | |
end | |
arg -= 1 | |
vi_prev_big_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_end_big_word(key, arg: 1, inclusive: false) | |
if @line.bytesize > @byte_pointer | |
byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
arg -= 1 | |
if inclusive and arg.zero? | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
if byte_size > 0 | |
c = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(c) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
end | |
vi_end_big_word(key, arg: arg) if arg > 0 | |
end | |
private def vi_delete_prev_char(key) | |
if @is_multiline and @cursor == 0 and @line_index > 0 | |
@buffer_of_lines[@line_index] = @line | |
@cursor = calculate_width(@buffer_of_lines[@line_index - 1]) | |
@byte_pointer = @buffer_of_lines[@line_index - 1].bytesize | |
@buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) | |
@line_index -= 1 | |
@line = @buffer_of_lines[@line_index] | |
@cursor_max = calculate_width(@line) | |
@rerender_all = true | |
elsif @cursor > 0 | |
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor -= width | |
@cursor_max -= width | |
end | |
end | |
private def vi_insert_at_bol(key) | |
ed_move_to_beg(key) | |
@config.editing_mode = :vi_insert | |
end | |
private def vi_add_at_eol(key) | |
ed_move_to_end(key) | |
@config.editing_mode = :vi_insert | |
end | |
private def ed_delete_prev_char(key, arg: 1) | |
deleted = '' | |
arg.times do | |
if @cursor > 0 | |
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) | |
@byte_pointer -= byte_size | |
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size) | |
deleted.prepend(mbchar) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor -= width | |
@cursor_max -= width | |
end | |
end | |
copy_for_vi(deleted) | |
end | |
private def vi_zero(key) | |
@byte_pointer = 0 | |
@cursor = 0 | |
end | |
private def vi_change_meta(key, arg: 1) | |
@waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| | |
if byte_pointer_diff > 0 | |
@line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) | |
elsif byte_pointer_diff < 0 | |
@line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) | |
end | |
copy_for_vi(cut) | |
@cursor += cursor_diff if cursor_diff < 0 | |
@cursor_max -= cursor_diff.abs | |
@byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 | |
@config.editing_mode = :vi_insert | |
} | |
@waiting_operator_vi_arg = arg | |
end | |
private def vi_delete_meta(key, arg: 1) | |
@waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| | |
if byte_pointer_diff > 0 | |
@line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) | |
elsif byte_pointer_diff < 0 | |
@line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) | |
end | |
copy_for_vi(cut) | |
@cursor += cursor_diff if cursor_diff < 0 | |
@cursor_max -= cursor_diff.abs | |
@byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 | |
} | |
@waiting_operator_vi_arg = arg | |
end | |
private def vi_yank(key, arg: 1) | |
@waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| | |
if byte_pointer_diff > 0 | |
cut = @line.byteslice(@byte_pointer, byte_pointer_diff) | |
elsif byte_pointer_diff < 0 | |
cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) | |
end | |
copy_for_vi(cut) | |
} | |
@waiting_operator_vi_arg = arg | |
end | |
private def vi_list_or_eof(key) | |
if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) | |
@line = nil | |
if @buffer_of_lines.size > 1 | |
scroll_down(@highest_in_all - @first_line_started_from) | |
end | |
Reline::IOGate.move_cursor_column(0) | |
@eof = true | |
finish | |
else | |
ed_newline(key) | |
end | |
end | |
alias_method :vi_end_of_transmission, :vi_list_or_eof | |
alias_method :vi_eof_maybe, :vi_list_or_eof | |
private def ed_delete_next_char(key, arg: 1) | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
unless @line.empty? || byte_size == 0 | |
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size) | |
copy_for_vi(mbchar) | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
@cursor_max -= width | |
if @cursor > 0 and @cursor >= @cursor_max | |
@byte_pointer -= byte_size | |
@cursor -= width | |
end | |
end | |
arg -= 1 | |
ed_delete_next_char(key, arg: arg) if arg > 0 | |
end | |
private def vi_to_history_line(key) | |
if Reline::HISTORY.empty? | |
return | |
end | |
if @history_pointer.nil? | |
@history_pointer = 0 | |
@line_backup_in_history = @line | |
@line = Reline::HISTORY[@history_pointer] | |
@cursor_max = calculate_width(@line) | |
@cursor = 0 | |
@byte_pointer = 0 | |
elsif @history_pointer.zero? | |
return | |
else | |
Reline::HISTORY[@history_pointer] = @line | |
@history_pointer = 0 | |
@line = Reline::HISTORY[@history_pointer] | |
@cursor_max = calculate_width(@line) | |
@cursor = 0 | |
@byte_pointer = 0 | |
end | |
end | |
private def vi_histedit(key) | |
path = Tempfile.open { |fp| | |
fp.write @line | |
fp.path | |
} | |
system("#{ENV['EDITOR']} #{path}") | |
@line = File.read(path) | |
finish | |
end | |
private def vi_paste_prev(key, arg: 1) | |
if @vi_clipboard.size > 0 | |
@line = byteinsert(@line, @byte_pointer, @vi_clipboard) | |
@cursor_max += calculate_width(@vi_clipboard) | |
cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join | |
@cursor += calculate_width(cursor_point) | |
@byte_pointer += cursor_point.bytesize | |
end | |
arg -= 1 | |
vi_paste_prev(key, arg: arg) if arg > 0 | |
end | |
private def vi_paste_next(key, arg: 1) | |
if @vi_clipboard.size > 0 | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
@line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard) | |
@cursor_max += calculate_width(@vi_clipboard) | |
@cursor += calculate_width(@vi_clipboard) | |
@byte_pointer += @vi_clipboard.bytesize | |
end | |
arg -= 1 | |
vi_paste_next(key, arg: arg) if arg > 0 | |
end | |
private def ed_argument_digit(key) | |
if @vi_arg.nil? | |
unless key.chr.to_i.zero? | |
@vi_arg = key.chr.to_i | |
end | |
else | |
@vi_arg = @vi_arg * 10 + key.chr.to_i | |
end | |
end | |
private def vi_to_column(key, arg: 0) | |
@byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc| | |
# total has [byte_size, cursor] | |
mbchar_width = Reline::Unicode.get_mbchar_width(gc) | |
if (total.last + mbchar_width) >= arg | |
break total | |
elsif (total.last + mbchar_width) >= @cursor_max | |
break total | |
else | |
total = [total.first + gc.bytesize, total.last + mbchar_width] | |
total | |
end | |
} | |
end | |
private def vi_replace_char(key, arg: 1) | |
@waiting_proc = ->(k) { | |
if arg == 1 | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
before = @line.byteslice(0, @byte_pointer) | |
remaining_point = @byte_pointer + byte_size | |
after = @line.byteslice(remaining_point, @line.size - remaining_point) | |
@line = before + k.chr + after | |
@cursor_max = calculate_width(@line) | |
@waiting_proc = nil | |
elsif arg > 1 | |
byte_size = 0 | |
arg.times do | |
byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size) | |
end | |
before = @line.byteslice(0, @byte_pointer) | |
remaining_point = @byte_pointer + byte_size | |
after = @line.byteslice(remaining_point, @line.size - remaining_point) | |
replaced = k.chr * arg | |
@line = before + replaced + after | |
@byte_pointer += replaced.bytesize | |
@cursor += calculate_width(replaced) | |
@cursor_max = calculate_width(@line) | |
@waiting_proc = nil | |
end | |
} | |
end | |
private def vi_next_char(key, arg: 1, inclusive: false) | |
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) } | |
end | |
private def vi_to_next_char(key, arg: 1, inclusive: false) | |
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) } | |
end | |
private def search_next_char(key, arg, need_prev_char: false, inclusive: false) | |
if key.instance_of?(String) | |
inputed_char = key | |
else | |
inputed_char = key.chr | |
end | |
prev_total = nil | |
total = nil | |
found = false | |
@line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| | |
# total has [byte_size, cursor] | |
unless total | |
# skip cursor point | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
total = [mbchar.bytesize, width] | |
else | |
if inputed_char == mbchar | |
arg -= 1 | |
if arg.zero? | |
found = true | |
break | |
end | |
end | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
prev_total = total | |
total = [total.first + mbchar.bytesize, total.last + width] | |
end | |
end | |
if not need_prev_char and found and total | |
byte_size, width = total | |
@byte_pointer += byte_size | |
@cursor += width | |
elsif need_prev_char and found and prev_total | |
byte_size, width = prev_total | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
if inclusive | |
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) | |
if byte_size > 0 | |
c = @line.byteslice(@byte_pointer, byte_size) | |
width = Reline::Unicode.get_mbchar_width(c) | |
@byte_pointer += byte_size | |
@cursor += width | |
end | |
end | |
@waiting_proc = nil | |
end | |
private def vi_prev_char(key, arg: 1) | |
@waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) } | |
end | |
private def vi_to_prev_char(key, arg: 1) | |
@waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) } | |
end | |
private def search_prev_char(key, arg, need_next_char = false) | |
if key.instance_of?(String) | |
inputed_char = key | |
else | |
inputed_char = key.chr | |
end | |
prev_total = nil | |
total = nil | |
found = false | |
@line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| | |
# total has [byte_size, cursor] | |
unless total | |
# skip cursor point | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
total = [mbchar.bytesize, width] | |
else | |
if inputed_char == mbchar | |
arg -= 1 | |
if arg.zero? | |
found = true | |
break | |
end | |
end | |
width = Reline::Unicode.get_mbchar_width(mbchar) | |
prev_total = total | |
total = [total.first + mbchar.bytesize, total.last + width] | |
end | |
end | |
if not need_next_char and found and total | |
byte_size, width = total | |
@byte_pointer -= byte_size | |
@cursor -= width | |
elsif need_next_char and found and prev_total | |
byte_size, width = prev_total | |
@byte_pointer -= byte_size | |
@cursor -= width | |
end | |
@waiting_proc = nil | |
end | |
private def vi_join_lines(key, arg: 1) | |
if @is_multiline and @buffer_of_lines.size > @line_index + 1 | |
@cursor = calculate_width(@line) | |
@byte_pointer = @line.bytesize | |
@line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip | |
@cursor_max = calculate_width(@line) | |
@buffer_of_lines[@line_index] = @line | |
@rerender_all = true | |
@rest_height += 1 | |
end | |
arg -= 1 | |
vi_join_lines(key, arg: arg) if arg > 0 | |
end | |
private def em_set_mark(key) | |
@mark_pointer = [@byte_pointer, @line_index] | |
end | |
alias_method :set_mark, :em_set_mark | |
private def em_exchange_mark(key) | |
return unless @mark_pointer | |
new_pointer = [@byte_pointer, @line_index] | |
@previous_line_index = @line_index | |
@byte_pointer, @line_index = @mark_pointer | |
@cursor = calculate_width(@line.byteslice(0, @byte_pointer)) | |
@cursor_max = calculate_width(@line) | |
@mark_pointer = new_pointer | |
end | |
alias_method :exchange_point_and_mark, :em_exchange_mark | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment