1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/reline/line_editor.rb
aycabta cafa7904e7
Backport lib/reline, and lib/irb for 3.0.1 4th (#4349)
* [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>
2021-04-03 18:26:46 +09:00

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