mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00

* [ruby/irb] Update help message for next context-mode of 4 While here, fixing tab/space issues in help message, and sync rdoc for IRB class to match the help message. https://github.com/ruby/irb/commit/ef8e3901cc * [ruby/irb] Do not continue line if last expression is an endless range Fixes [Bug #14824] https://github.com/ruby/irb/commit/63414f8465 * [ruby/irb] Add a test for not continuing when endless range at eol https://github.com/ruby/irb/commit/1020ac9c65 * [ruby/irb] Make save-history extension safe for concurrent use This makes the save-history extension check for modifications to the history file before saving it. If the history file was modified after the history was loaded and before it was saved, append only the new history lines to the history file. This can result in more lines in the history file than SAVE_HISTORY allows. However, that will be fixed the next time irb is run and the history is saved. Fixes [Bug #13654] https://github.com/ruby/irb/commit/041ef53845 * Fix errors when XDG_CONFIG_HOME points to non-writable directory `$HOME/.config` is not writable on CI because I think tests should not corrupt user's data. And GitHub Actions CI sets `XDG_CONFIG_HOME` since `Version: 20210309.1`. https://github.com/ruby/actions/runs/2130811016?check_suite_focus=true#step:16:301 ``` Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb ``` * Try to fix errors in TestIRB::TestHistory too https://github.com/ruby/actions/runs/2137935523?check_suite_focus=true#step:9:562 ``` 1) Error: TestIRB::TestHistory#test_history_concurrent_use: Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb /home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `mkdir' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `fu_mkdir' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:231:in `block (2 levels) in mkdir_p' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `reverse_each' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `block in mkdir_p' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `each' /home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `mkdir_p' /home/runner/work/actions/actions/ruby/lib/irb/init.rb:355:in `rc_file_generators' /home/runner/work/actions/actions/ruby/lib/irb/init.rb:330:in `rc_file' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:170:in `block in assert_history' /home/runner/work/actions/actions/ruby/lib/tmpdir.rb:96:in `mktmpdir' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:168:in `assert_history' /home/runner/work/actions/actions/ruby/test/irb/test_history.rb:133:in `test_history_concurrent_use' ``` * [ruby/irb] Define "measure" command without forced override https://github.com/ruby/irb/commit/9587ba13b5 * [ruby/irb] Add all lib files automatically https://github.com/ruby/irb/commit/ecc82336b7 * [ruby/irb] Don't call Ruby 2.4+'s String#pretty_print https://github.com/ruby/irb/commit/89bcf107be * [ruby/irb] Implement ls command https://github.com/ruby/irb/commit/19b6c20604 * [ruby/irb] Add whereami command https://github.com/ruby/irb/commit/bc822e4aac * [ruby/irb] Fix column overflow on ls output https://github.com/ruby/irb/commit/6115754623 * [ruby/irb] Fix step's argument cols.size was calling Integer#size, which returns 8. Fixing a bug of https://github.com/ruby/irb/pull/209 https://github.com/ruby/irb/commit/c93ae4be71 * [ruby/irb] Deal with different screen sizes e.g. http://rubyci.s3.amazonaws.com/centos8/ruby-master/log/20210321T063003Z.fail.html.gz https://github.com/ruby/irb/commit/ddb3472ba2 * [ruby/irb] Have some right padding instead of filling out an entire line https://github.com/ruby/irb/commit/6ac8f45f5f * Suppress verbose messages Get rid of warnings in the parallel test. ``` unknown command: "Switch to inspect mode." ``` * [ruby/irb] Change ripper_lex_without_warning to a class method https://github.com/ruby/irb/commit/d9f8abc17e * [ruby/irb] Complete require and require_relative https://github.com/ruby/irb/commit/1c61178b4c * [ruby/reline] Add Reline.ungetc to control buffer https://github.com/ruby/reline/commit/43ac03c624 * [ruby/reline] Reline.delete_text removes the current line in multiline https://github.com/ruby/reline/commit/da90c094a1 * [ruby/reline] Support preposing and postposing for Reline.completion_proc https://github.com/ruby/reline/commit/1f469de90c * [ruby/reline] Suppress crashing when completer_{quote,word_break}_characters is empty https://github.com/ruby/reline/commit/c6f1164942 * [ruby/irb] fix completion test when out-of-place build * [ruby/irb] Cache completion files to require https://github.com/ruby/irb/commit/612ebcb311 * [ruby/irb] Always add input method when calling Irb.new in tests When passes input method as nil to Context.new through Irb.new, ReidlineInputMethod.new is executed and the global internal state of Reline is rewritten, therefore other tests are failed in the Ruby repository. This commit changes to use TestInputMethod. https://github.com/ruby/irb/commit/010dce9210 * [ruby/irb] Prevent the completion from crashing if rdoc is missing There are cases where ruby is installed without rdoc and e.g. lib/irb/cmd/help.rb also handles the LoadError Here is how to replicate the issue: ``` $ docker run -it alpine:3.13.3 sh / # apk add ruby ruby-irb ruby-io-console / # irb irb(main):001:0> Class[TAB][TAB] ``` And you end up with something like: ``` irb(main):001:0> ClassTraceback (most recent call last): 34: from /usr/bin/irb:23:in `<main>' 33: from /usr/bin/irb:23:in `load' 32: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>' 31: from /usr/lib/ruby/2.7.0/irb.rb:400:in `start' 30: from /usr/lib/ruby/2.7.0/irb.rb:471:in `run' 29: from /usr/lib/ruby/2.7.0/irb.rb:471:in `catch' 28: from /usr/lib/ruby/2.7.0/irb.rb:472:in `block in run' 27: from /usr/lib/ruby/2.7.0/irb.rb:537:in `eval_input' 26: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `each_top_level_statement' 25: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `catch' 24: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `block in each_top_level_statement' 23: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `loop' 22: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:154:in `block (2 levels) in each_top_level_statement' 21: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:182:in `lex' 20: from /usr/lib/ruby/2.7.0/irb.rb:518:in `block in eval_input' 19: from /usr/lib/ruby/2.7.0/irb.rb:704:in `signal_status' 18: from /usr/lib/ruby/2.7.0/irb.rb:519:in `block (2 levels) in eval_input' 17: from /usr/lib/ruby/2.7.0/irb/input-method.rb:294:in `gets' 16: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline' 15: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline' 14: from /usr/lib/ruby/2.7.0/reline.rb:175:in `readmultiline' 13: from /usr/lib/ruby/2.7.0/reline.rb:238:in `inner_readline' 12: from /usr/lib/ruby/2.7.0/reline.rb:238:in `loop' 11: from /usr/lib/ruby/2.7.0/reline.rb:239:in `block in inner_readline' 10: from /usr/lib/ruby/2.7.0/reline.rb:270:in `read_io' 9: from /usr/lib/ruby/2.7.0/reline.rb:270:in `loop' 8: from /usr/lib/ruby/2.7.0/reline.rb:311:in `block in read_io' 7: from /usr/lib/ruby/2.7.0/reline.rb:240:in `block (2 levels) in inner_readline' 6: from /usr/lib/ruby/2.7.0/reline.rb:240:in `each' 5: from /usr/lib/ruby/2.7.0/reline.rb:241:in `block (3 levels) in inner_readline' 4: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:820:in `input_key' 3: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:608:in `complete' 2: from /usr/lib/ruby/2.7.0/irb/completion.rb:269:in `block in <module:InputCompletor>' 1: from /usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require' /usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require': cannot load such file -- rdoc (LoadError) ``` https://github.com/ruby/irb/commit/a2d299c2ac * [ruby/irb] Suppress verbose messages in the parallel test `:VERBOSE` flag needs to be set prior to `IRB::Irb.new`. https://github.com/ruby/irb/commit/78604682d9 * [ruby/irb] SIGINT should raise Interrupt after IRB session https://github.com/ruby/irb/commit/5832cfe75b * [ruby/irb] Colorize `__END__` as keyword https://github.com/ruby/irb/commit/9b84018311 * [ruby/irb] Add show_source command https://github.com/ruby/irb/commit/108cb04352 * [ruby/reline] Reset @rest_height when clear screen https://github.com/ruby/reline/commit/3a7019b0d5 * [ruby/irb] process multi-line pastes as a single entity this allows pasting leading-dot chained methods correctly: ```ruby class A def a; self; end def b; true; end end a = A.new a .a .b ``` will properly return `true` instead of erroring on the `.a` line: ``` irb(main):001:1* class A irb(main):002:1* def a; self; end irb(main):003:0> end irb(main):004:0* irb(main):005:0> a = A.new irb(main):006:0* irb(main):007:0> a irb(main):008:0> .a irb(main):009:0> .a => #<A:0x00007f984211fbe8> ``` https://github.com/ruby/irb/commit/45aeb52575 * [ruby/irb] Add yamatanooroti test example https://github.com/ruby/irb/commit/279155fcee * [ruby/irb] Add test for multiline paste https://github.com/ruby/irb/commit/e93c9cb54d * [ruby/irb] Evaluate each toplevel statement https://github.com/ruby/irb/commit/bc1b1d8bc3 * [ruby/irb] Version 1.3.5 https://github.com/ruby/irb/commit/22e2ddf715 * [ruby/reline] Version 0.2.5 https://github.com/ruby/reline/commit/22ce5651e5 Co-authored-by: Jeremy Evans <code@jeremyevans.net> Co-authored-by: Kazuhiro NISHIYAMA <zn@mbf.nifty.com> Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com> Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org> Co-authored-by: Aleksandar Ivanov <aivanov92@gmail.com> Co-authored-by: Koichi Sasada <ko1@atdot.net> Co-authored-by: Cody Cutrer <cody@instructure.com>
2781 lines
91 KiB
Ruby
2781 lines
91 KiB
Ruby
require 'reline/kill_ring'
|
|
require 'reline/unicode'
|
|
|
|
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 set_pasting_state(in_pasting)
|
|
@in_pasting = in_pasting
|
|
end
|
|
|
|
def simplified_rendering?
|
|
if finished?
|
|
false
|
|
elsif @just_cursor_moving and not @rerender_all
|
|
true
|
|
else
|
|
not @rerender_all and not finished? and @in_pasting
|
|
end
|
|
end
|
|
|
|
private def check_mode_string
|
|
mode_string = nil
|
|
if @config.show_mode_in_prompt
|
|
if @config.editing_mode_is?(:vi_command)
|
|
mode_string = @config.vi_cmd_mode_string
|
|
elsif @config.editing_mode_is?(:vi_insert)
|
|
mode_string = @config.vi_ins_mode_string
|
|
elsif @config.editing_mode_is?(:emacs)
|
|
mode_string = @config.emacs_mode_string
|
|
else
|
|
mode_string = '?'
|
|
end
|
|
end
|
|
if mode_string != @prev_mode_string
|
|
@rerender_all = true
|
|
end
|
|
@prev_mode_string = mode_string
|
|
mode_string
|
|
end
|
|
|
|
private def check_multiline_prompt(buffer, prompt)
|
|
if @vi_arg
|
|
prompt = "(arg: #{@vi_arg}) "
|
|
@rerender_all = true
|
|
elsif @searching_prompt
|
|
prompt = @searching_prompt
|
|
@rerender_all = true
|
|
else
|
|
prompt = @prompt
|
|
end
|
|
if simplified_rendering?
|
|
mode_string = check_mode_string
|
|
prompt = mode_string + prompt if mode_string
|
|
return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
|
|
end
|
|
if @prompt_proc
|
|
use_cached_prompt_list = false
|
|
if @cached_prompt_list
|
|
if @just_cursor_moving
|
|
use_cached_prompt_list = true
|
|
elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
|
|
use_cached_prompt_list = true
|
|
end
|
|
end
|
|
use_cached_prompt_list = false if @rerender_all
|
|
if use_cached_prompt_list
|
|
prompt_list = @cached_prompt_list
|
|
else
|
|
prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
|
|
@prompt_cache_time = Time.now.to_f
|
|
end
|
|
prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
|
|
prompt_list = [prompt] if prompt_list.empty?
|
|
mode_string = check_mode_string
|
|
prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
|
|
prompt = prompt_list[@line_index]
|
|
prompt = prompt_list[0] if prompt.nil?
|
|
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_width = calculate_width(prompt, true)
|
|
[prompt, prompt_width, prompt_list]
|
|
else
|
|
mode_string = check_mode_string
|
|
prompt = mode_string + prompt if mode_string
|
|
prompt_width = calculate_width(prompt, true)
|
|
[prompt, prompt_width, nil]
|
|
end
|
|
end
|
|
|
|
def reset(prompt = '', encoding:)
|
|
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
|
|
@screen_size = Reline::IOGate.get_screen_size
|
|
@screen_height = @screen_size.first
|
|
reset_variables(prompt, encoding: encoding)
|
|
@old_trap = Signal.trap('SIGINT') {
|
|
if @scroll_partial_screen
|
|
move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
|
|
else
|
|
move_cursor_down(@highest_in_all - @line_index - 1)
|
|
end
|
|
Reline::IOGate.move_cursor_column(0)
|
|
scroll_down(1)
|
|
@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
|
|
@screen_height = @screen_size.first
|
|
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)
|
|
@scroll_partial_screen = nil
|
|
@prev_mode_string = nil
|
|
@drop_terminate_spaces = false
|
|
@in_pasting = false
|
|
@auto_indent_proc = nil
|
|
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(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
|
|
new_cursor_max = calculate_width(line_to_calc)
|
|
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_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
|
|
new_started_from = height - 1
|
|
if update
|
|
@cursor = new_cursor
|
|
@cursor_max = new_cursor_max
|
|
@started_from = new_started_from
|
|
@byte_pointer = new_byte_pointer
|
|
else
|
|
[new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
|
|
end
|
|
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
|
|
if @is_multiline and finished? and @scroll_partial_screen
|
|
# Re-output all code higher than the screen when finished.
|
|
Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
@scroll_partial_screen = nil
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
|
|
if @previous_line_index
|
|
new_lines = whole_lines(index: @previous_line_index, line: @line)
|
|
else
|
|
new_lines = whole_lines
|
|
end
|
|
modify_lines(new_lines).each_with_index do |line, index|
|
|
@output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
|
|
Reline::IOGate.erase_after_cursor
|
|
end
|
|
@output.flush
|
|
return
|
|
end
|
|
new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
|
|
# FIXME: end of logical line sometimes breaks
|
|
rendered = false
|
|
if @add_newline_to_end_of_buffer
|
|
rerender_added_newline(prompt, prompt_width)
|
|
@add_newline_to_end_of_buffer = false
|
|
else
|
|
if @just_cursor_moving and not @rerender_all
|
|
rendered = 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
|
|
else
|
|
end
|
|
end
|
|
if @is_multiline
|
|
if finished?
|
|
# Always rerender on finish because output_modifier_proc may return a different output.
|
|
if @previous_line_index
|
|
new_lines = whole_lines(index: @previous_line_index, line: @line)
|
|
else
|
|
new_lines = whole_lines
|
|
end
|
|
line = modify_lines(new_lines)[@line_index]
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
|
|
render_partial(prompt, prompt_width, line, @first_line_started_from)
|
|
move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
|
|
scroll_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
Reline::IOGate.erase_after_cursor
|
|
elsif not rendered
|
|
unless @in_pasting
|
|
line = modify_lines(whole_lines)[@line_index]
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
|
|
render_partial(prompt, prompt_width, line, @first_line_started_from)
|
|
end
|
|
end
|
|
@buffer_of_lines[@line_index] = @line
|
|
@rest_height = 0 if @scroll_partial_screen
|
|
else
|
|
line = modify_lines(whole_lines)[@line_index]
|
|
render_partial(prompt, prompt_width, line, 0)
|
|
if finished?
|
|
scroll_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
Reline::IOGate.erase_after_cursor
|
|
end
|
|
end
|
|
end
|
|
|
|
private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
|
|
if @screen_height < highest_in_all
|
|
old_scroll_partial_screen = @scroll_partial_screen
|
|
if cursor_y == 0
|
|
@scroll_partial_screen = 0
|
|
elsif cursor_y == (highest_in_all - 1)
|
|
@scroll_partial_screen = highest_in_all - @screen_height
|
|
else
|
|
if @scroll_partial_screen
|
|
if cursor_y <= @scroll_partial_screen
|
|
@scroll_partial_screen = cursor_y
|
|
elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
|
|
@scroll_partial_screen = cursor_y - (@screen_height - 1)
|
|
end
|
|
else
|
|
if cursor_y > (@screen_height - 1)
|
|
@scroll_partial_screen = cursor_y - (@screen_height - 1)
|
|
else
|
|
@scroll_partial_screen = 0
|
|
end
|
|
end
|
|
end
|
|
if @scroll_partial_screen != old_scroll_partial_screen
|
|
@rerender_all = true
|
|
end
|
|
else
|
|
if @scroll_partial_screen
|
|
@rerender_all = true
|
|
end
|
|
@scroll_partial_screen = nil
|
|
end
|
|
end
|
|
|
|
private def rerender_added_newline(prompt, prompt_width)
|
|
scroll_down(1)
|
|
@buffer_of_lines[@previous_line_index] = @line
|
|
@line = @buffer_of_lines[@line_index]
|
|
unless @in_pasting
|
|
render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
|
|
end
|
|
@cursor = @cursor_max = calculate_width(@line)
|
|
@byte_pointer = @line.bytesize
|
|
@highest_in_all += @highest_in_this
|
|
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
|
|
@first_line_started_from += @started_from + 1
|
|
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
|
@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
|
|
first_line_diff = new_first_line_started_from - @first_line_started_from
|
|
new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
|
|
new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
|
|
calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
|
|
@previous_line_index = nil
|
|
if @rerender_all
|
|
@line = @buffer_of_lines[@line_index]
|
|
rerender_all_lines
|
|
@rerender_all = false
|
|
true
|
|
else
|
|
@line = @buffer_of_lines[@line_index]
|
|
@first_line_started_from = new_first_line_started_from
|
|
@started_from = new_started_from
|
|
@cursor = new_cursor
|
|
@cursor_max = new_cursor_max
|
|
@byte_pointer = new_byte_pointer
|
|
move_cursor_down(first_line_diff + @started_from)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
false
|
|
end
|
|
end
|
|
|
|
private def rerender_changed_current_line
|
|
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
|
|
old_highest_in_all = @highest_in_all
|
|
if @line_index.zero?
|
|
new_first_line_started_from = 0
|
|
else
|
|
new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
|
|
end
|
|
new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
|
calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
|
|
if @scroll_partial_screen
|
|
move_cursor_up(@first_line_started_from + @started_from)
|
|
scroll_down(@screen_height - 1)
|
|
move_cursor_up(@screen_height)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
elsif back > old_highest_in_all
|
|
scroll_down(back - 1)
|
|
move_cursor_up(back - 1)
|
|
elsif back < old_highest_in_all
|
|
scroll_down(back)
|
|
Reline::IOGate.erase_after_cursor
|
|
(old_highest_in_all - back - 1).times do
|
|
scroll_down(1)
|
|
Reline::IOGate.erase_after_cursor
|
|
end
|
|
move_cursor_up(old_highest_in_all - 1)
|
|
end
|
|
render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
|
|
if @prompt_proc
|
|
prompt = prompt_list[@line_index]
|
|
prompt_width = calculate_width(prompt, true)
|
|
end
|
|
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
|
|
@highest_in_all = back
|
|
@first_line_started_from = new_first_line_started_from
|
|
@started_from = new_started_from
|
|
if @scroll_partial_screen
|
|
Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
else
|
|
move_cursor_down(@first_line_started_from + @started_from - back + 1)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
end
|
|
end
|
|
|
|
private def render_whole_lines(lines, prompt, prompt_width)
|
|
rendered_height = 0
|
|
modify_lines(lines).each_with_index do |line, index|
|
|
if prompt.is_a?(Array)
|
|
line_prompt = prompt[index]
|
|
prompt_width = calculate_width(line_prompt, true)
|
|
else
|
|
line_prompt = prompt
|
|
end
|
|
height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
|
|
if index < (lines.size - 1)
|
|
if @scroll_partial_screen
|
|
if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
|
|
move_cursor_down(1)
|
|
end
|
|
else
|
|
scroll_down(1)
|
|
end
|
|
rendered_height += height
|
|
else
|
|
rendered_height += height - 1
|
|
end
|
|
end
|
|
rendered_height
|
|
end
|
|
|
|
private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
|
|
visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
|
|
cursor_up_from_last_line = 0
|
|
# TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
|
|
if @scroll_partial_screen
|
|
last_visual_line = this_started_from + (height - 1)
|
|
last_screen_line = @scroll_partial_screen + (@screen_height - 1)
|
|
if (@scroll_partial_screen - this_started_from) >= height
|
|
# Render nothing because this line is before the screen.
|
|
visual_lines = []
|
|
elsif this_started_from > last_screen_line
|
|
# Render nothing because this line is after the screen.
|
|
visual_lines = []
|
|
else
|
|
deleted_lines_before_screen = []
|
|
if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
|
|
# A part of visual lines are before the screen.
|
|
deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
|
|
deleted_lines_before_screen.compact!
|
|
end
|
|
if this_started_from <= last_screen_line and last_screen_line < last_visual_line
|
|
# A part of visual lines are after the screen.
|
|
visual_lines.pop((last_visual_line - last_screen_line) * 2)
|
|
end
|
|
move_cursor_up(deleted_lines_before_screen.size - @started_from)
|
|
cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
|
|
end
|
|
end
|
|
if with_control
|
|
if height > @highest_in_this
|
|
diff = height - @highest_in_this
|
|
scroll_down(diff)
|
|
@highest_in_all += diff
|
|
@highest_in_this = height
|
|
move_cursor_up(diff)
|
|
elsif height < @highest_in_this
|
|
diff = @highest_in_this - height
|
|
@highest_in_all -= diff
|
|
@highest_in_this = height
|
|
end
|
|
move_cursor_up(@started_from)
|
|
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
|
cursor_up_from_last_line = height - 1 - @started_from
|
|
end
|
|
if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
|
|
@output.write "\e[0m" # clear character decorations
|
|
end
|
|
visual_lines.each_with_index do |line, index|
|
|
Reline::IOGate.move_cursor_column(0)
|
|
if line.nil?
|
|
if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
|
|
# reaches the end of line
|
|
if Reline::IOGate.win? and Reline::IOGate.win_legacy_console?
|
|
# A newline is automatically inserted if a character is rendered at
|
|
# eol on command prompt.
|
|
else
|
|
# When the cursor is at the end of the line and erases characters
|
|
# after the cursor, some terminals delete the character at the
|
|
# cursor position.
|
|
move_cursor_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
end
|
|
else
|
|
Reline::IOGate.erase_after_cursor
|
|
move_cursor_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
end
|
|
next
|
|
end
|
|
@output.write line
|
|
if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
|
|
# A newline is automatically inserted if a character is rendered at eol on command prompt.
|
|
@rest_height -= 1 if @rest_height > 0
|
|
end
|
|
@output.flush
|
|
if @first_prompt
|
|
@first_prompt = false
|
|
@pre_input_hook&.call
|
|
end
|
|
end
|
|
unless visual_lines.empty?
|
|
Reline::IOGate.erase_after_cursor
|
|
Reline::IOGate.move_cursor_column(0)
|
|
end
|
|
if with_control
|
|
# Just after rendring, so the cursor is on the last line.
|
|
if finished?
|
|
Reline::IOGate.move_cursor_column(0)
|
|
else
|
|
# Moves up from bottom of lines to the cursor position.
|
|
move_cursor_up(cursor_up_from_last_line)
|
|
# This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
end
|
|
end
|
|
height
|
|
end
|
|
|
|
private def modify_lines(before)
|
|
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, back, with_control: false)
|
|
else
|
|
height = render_partial(prompt, prompt_width, line, back, with_control: false)
|
|
end
|
|
if index < (@buffer_of_lines.size - 1)
|
|
move_cursor_down(height)
|
|
back += height
|
|
end
|
|
end
|
|
move_cursor_up(back)
|
|
move_cursor_down(@first_line_started_from + @started_from)
|
|
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
end
|
|
|
|
def editing_mode
|
|
@config.editing_mode
|
|
end
|
|
|
|
private def menu(target, list)
|
|
@menu_info = MenuInfo.new(target, list)
|
|
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
|
|
else
|
|
ed_insert(key) unless @config.editing_mode_is?(:vi_command)
|
|
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
|
|
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
|
|
# 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
|
|
old_line = @line.dup
|
|
@first_char = false
|
|
completion_occurs = false
|
|
if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
|
|
unless @config.disable_completion
|
|
result = call_completion_proc
|
|
if result.is_a?(Array)
|
|
completion_occurs = true
|
|
process_insert
|
|
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
|
|
normal_char(key)
|
|
end
|
|
unless completion_occurs
|
|
@completion_state = CompletionState::NORMAL
|
|
end
|
|
if not @in_pasting and @just_cursor_moving.nil?
|
|
if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
|
|
@just_cursor_moving = true
|
|
elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
|
|
@just_cursor_moving = true
|
|
else
|
|
@just_cursor_moving = false
|
|
end
|
|
else
|
|
@just_cursor_moving = false
|
|
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)
|
|
preposing, target, postposing = result
|
|
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, preposing)
|
|
when 3..Float::INFINITY
|
|
result = @completion_proc.(target, preposing, postposing)
|
|
end
|
|
end
|
|
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)
|
|
new_indent = @cursor_max if new_indent&.> @cursor_max
|
|
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)
|
|
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 = @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 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 = @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
|
|
if @is_multiline
|
|
if @previous_line_index
|
|
lines = whole_lines(index: @previous_line_index, line: @line)
|
|
else
|
|
lines = whole_lines
|
|
end
|
|
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
|
|
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?
|
|
if @is_multiline
|
|
if @buffer_of_lines.size == 1
|
|
@line&.clear
|
|
@byte_pointer = 0
|
|
@cursor = 0
|
|
@cursor_max = 0
|
|
elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
|
|
@buffer_of_lines.pop
|
|
@line_index -= 1
|
|
@line = @buffer_of_lines[@line_index]
|
|
@byte_pointer = 0
|
|
@cursor = 0
|
|
@cursor_max = calculate_width(@line)
|
|
elsif @line_index < (@buffer_of_lines.size - 1)
|
|
@buffer_of_lines.delete_at(@line_index)
|
|
@line = @buffer_of_lines[@line_index]
|
|
@byte_pointer = 0
|
|
@cursor = 0
|
|
@cursor_max = calculate_width(@line)
|
|
end
|
|
else
|
|
@line&.clear
|
|
@byte_pointer = 0
|
|
@cursor = 0
|
|
@cursor_max = 0
|
|
end
|
|
elsif not start.nil? and not length.nil?
|
|
if @line
|
|
before = @line.byteslice(0, start)
|
|
after = @line.byteslice(start + length, @line.bytesize)
|
|
@line = before + after
|
|
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
|
|
str = @line.byteslice(0, @byte_pointer)
|
|
@cursor = calculate_width(str)
|
|
@cursor_max = calculate_width(@line)
|
|
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
|
|
if @previous_line_index
|
|
whole_lines(index: @previous_line_index, line: @line).join("\n")
|
|
else
|
|
whole_lines.join("\n")
|
|
end
|
|
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 unless @in_pasting
|
|
end
|
|
end
|
|
|
|
private def ed_unassigned(key) end # do nothing
|
|
|
|
private def process_insert(force: false)
|
|
return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
|
|
width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
|
|
bytesize = @continuous_insertion_buffer.bytesize
|
|
if @cursor == @cursor_max
|
|
@line += @continuous_insertion_buffer
|
|
else
|
|
@line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer)
|
|
end
|
|
@byte_pointer += bytesize
|
|
@cursor += width
|
|
@cursor_max += width
|
|
@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 @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
|
|
@line += str
|
|
else
|
|
@line = byteinsert(@line, @byte_pointer, str)
|
|
end
|
|
last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
|
|
@byte_pointer += bytesize
|
|
last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
|
|
if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
|
|
width = 0
|
|
end
|
|
@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)`': "
|
|
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
|
|
@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
|
|
@rerender_all = true
|
|
@cached_prompt_list = nil
|
|
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
|
|
@rerender_all = true
|
|
@cached_prompt_list = nil
|
|
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[@line_index]
|
|
@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[@line_index]
|
|
@rerender_all = true
|
|
else
|
|
if @history_pointer.nil? and substr.empty?
|
|
@line = ''
|
|
else
|
|
@line = Reline::HISTORY[@history_pointer]
|
|
end
|
|
end
|
|
@cursor_max = calculate_width(@line)
|
|
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
|
|
alias_method :kill_line, :em_kill_line
|
|
|
|
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
|
|
alias_method :yank, :em_yank
|
|
|
|
private def em_yank_pop(key)
|
|
yanked, prev_yank = @kill_ring.yank_pop
|
|
if yanked
|
|
prev_yank_width = calculate_width(prev_yank)
|
|
@cursor -= prev_yank_width
|
|
@cursor_max -= prev_yank_width
|
|
@byte_pointer -= prev_yank.bytesize
|
|
@line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
|
|
@line = byteinsert(@line, @byte_pointer, yanked)
|
|
yanked_width = calculate_width(yanked)
|
|
@cursor += yanked_width
|
|
@cursor_max += yanked_width
|
|
@byte_pointer += yanked.bytesize
|
|
end
|
|
end
|
|
alias_method :yank_pop, :em_yank_pop
|
|
|
|
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, 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 @line.bytesize > @byte_pointer
|
|
byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
|
|
@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)
|
|
@drop_terminate_spaces = true
|
|
@waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
|
|
if byte_pointer_diff > 0
|
|
@line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
|
|
elsif byte_pointer_diff < 0
|
|
@line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
|
|
end
|
|
copy_for_vi(cut)
|
|
@cursor += cursor_diff if cursor_diff < 0
|
|
@cursor_max -= cursor_diff.abs
|
|
@byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
|
|
@config.editing_mode = :vi_insert
|
|
@drop_terminate_spaces = false
|
|
}
|
|
@waiting_operator_vi_arg = arg
|
|
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_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
|
|
mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
|
|
width = Reline::Unicode.get_mbchar_width(mbchar)
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
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|
|
|
if @is_multiline
|
|
fp.write whole_lines.join("\n")
|
|
else
|
|
fp.write @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
|
|
@line = @buffer_of_lines[@line_index]
|
|
@rerender_all = true
|
|
else
|
|
@line = File.read(path)
|
|
end
|
|
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.bytesize - 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.bytesize - 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
|