mirror of
https://github.com/ruby/ruby.git
synced 2025-09-19 02:23:59 +02:00

* [ruby/reline] Sort completion list #### Legacy mode: ```console $ irb --legacy irb(main):001:0> l[TAB][TAB] lambda load local_variables loop ``` #### Before this patch: ```console $ irb irb(main):001:0> l[TAB][TAB] local_variables loop lambda load ``` #### After this patch: ```console $ irb irb(main):001:0> l[TAB][TAB] lambda load local_variables loop ```6074069c7d
* Drop an invalid char as UTF-8 * Add test_completion_with_indent_and_completer_quote_characters This is for8a705245e5
. * [ruby/irb] Add tests for RubyLex The set_auto_indent method calculates the correct number of spaces for indenting a line. We think there might be a few bugs in this method so we are testing the current functionality to make sure nothing breaks when we address those bugs. Example test failure: ``` 1) Failure: TestIRB::TestRubyLex#test_auto_indent [/Users/Ben/Projects/irb/test/irb/test_ruby_lex.rb:75]: Calculated the wrong number of spaces for: def each_top_level_statement initialize_input catch(:TERM_INPUT) do loop do begin prompt unless l = lex throw :TERM_INPUT if @line == '' else . <10> expected but was <12>. ```752d5597ab
* [ruby/reline] Degenerate the terminal size to [$LINES, $COLUMNS] if it is unknown This is a workaround for https://github.com/ruby/irb/issues/505725677d1a
* [ruby/irb] Fix newline depth with multiple braces This commit fixes the check_newline_depth_difference method to multiple open braces on one line into account. Before this change we were subtracting from the depth in check_newline_depth_difference on every open brace. This is the right thing to do if the opening and closing brace are on the same line. For example in a method definition we have an opening and closing parentheses we want to add 1 to our depth, and then remove it. ``` def foo() end ``` However this isn't the correct behavior when the brace spans multiple lines. If a brace spans multiple lines we don't want to subtract from check_newline_depth_difference and we want to treat the braces the same way as we do `end` and allow check_corresponding_token_depth to pop the correct depth. Example of bad behavior: ``` def foo() [ ] puts 'bar' end ``` Example of desired behavior: ``` def foo() [ ] puts 'bar' end ```7dc8af01e0
* text/readline/test_readline.rb - fix skip on Reline (#2743) TestRelineAsReadline#test_input_metachar passes on MinGW * Add "require 'openstruct'" what is forgotten * [ruby/irb] Fix lib name of OpenStruct1f3a84ab6b
* Add load path and require for ruby/ruby * Rescue EOFError If C-d is pressed before IRB is ready, IRB crashes because EOFError occurs. * Complete indented and quoted string correctly def foo ''.upca[TAB] This will be completed to be: def foo ''.upcase The indent was gone. This commit fixes the bug. * [ruby/irb] Fix crashing when multiple open braces per line https://github.com/ruby/irb/issues/55 If we had put multiple open braces on a line the with no closing brace spaces_of_nest array keeps getting '0' added to it. This means that when we pop off of this array we are saying that we should be in position zero for the next line. This is an issue because we don't always want to be in position 0 after a closing brace. Example: ``` [[[ ] ] ] ``` In the above example the 'spaces_of_nest' array looks like this after the first line is entered: [0,0,0]. We really want to be indented 4 spaces for the 1st closing brace 2 for the 2nd and 0 for the 3rd. i.e. we want it to be: [0,2,4]. We also saw this issue with a heredoc inside of an array. ``` [<<FOO] hello FOO ```80c69c8272
* Support history-size in .inputrc correctly * Introduce an abstracted structure about the encoding of Reline The command prompt on Windows always uses Unicode to take input and print output but most Reline implementation depends on Encoding.default_external. This commit introduces an abstracted structure about the encoding of Reline. * Remove an unused setting variable * Use Reline.encoding_system_needs if exists * Add tests for vi_insert and vi_add * Implement vi_insert_at_bol and vi_add_at_eol * [ruby/reline] Implement vi_to_next_char066ecb0a21
* [ruby/reline] Implement vi_prev_char and vi_to_prev_char0ad3ee63fa
* [ruby/readline-ext] Include ruby/assert.h in ruby/ruby.h so that assertions can be there4d44c12832
* Stop using minitest dependent methods * Skip a test that uses assert_ruby_status if it doesn't exist * Use omit instead of skip * Check DONT_RUN_RELINE_TEST envvar * [ruby/irb] Add newline_before_multiline_output9eb1801a66
* [ruby/irb] Fix compatibility with rails before 5.2 Rails before 5.2 added Array#append as an alias to Array#<< , so that it expects only one argument. However ruby-2.5 added Array#append as an alias to Array#push which takes any number of arguments. If irb completion is used in `rails c` (for example "IO.<tab>") it fails with: irb/completion.rb:206:in `<<': wrong number of arguments (given 3, expected 1) (ArgumentError) Using Array#push instead of Array#append fixes compatibility.5b7bbf9c34
* Reline: Use a more robust detection of MinTTY The previous detection per get_screen_size fails when stdout is passed to a pipe. That is the case when running ruby tests in parallel ("-j" switch). In this case Reline believes that it's running on MinTTY and the tests are running with ANSI IOGate instead of the Windows adapter on MINGW. So parallel test results were different to that of a single process. This commit fixes these differencies. The code is taken from git sources and translated to ruby. NtQueryObject() is replaced by GetFileInformationByHandleEx(), because NtQueryObject() is undocumented and is more difficult to use:c5a03b1e29/compat/winansi.c (L558)
* Reline: Fix changed test results due to change to UTF-8 on Windows In commitf8ea2860b0
the Reline encoding for native windows console was changed to hardcoded UTF-8. This caused failures in reline and readline tests, but they were hidden, because parallel ruby tests incorrectly used Reline::ANSI as IOGate. Tests failures were raised in single process mode, but not with -j switch. This patch corrects encodings on native Windows console. * [ruby/irb] [ruby/irb] Rewrite an expression to detect multilineed5cf375a6
5b7bbf9c34
* [ruby/reline] Implement vi_change_meta8538e0e10f
* Always refer to Reline::IOGate.encoding * Always use UTF-8 for Reline::GeneralIO on Windows * Use test_mode on Reline::History::Test for encoding * [ruby/reline] Support GNOME style Home/End key sequences [Bug #16510]788f0df845
* [ruby/irb] Add a new easter egg: dancing rubye37dc7e58e
* [ruby/irb] Exclude useless files from RDoc8f1ab2400c
* [ruby/irb] Exclude useless files from RDoc * Fix inaccuracy in encoding tests These tests assume Encoding.find('locale') == Encoding.find('external') and fail if they are distinct. * [ruby/reline] Fix Reline::Windows#scroll_down I mistook Right and Bottom.8be401c5f5
* [ruby/reline] Bypass cursor down when a char is rendered at eol on Windows A newline is automatically inserted if a character is rendered at eol on Windows command prompt.4bfea07e4a
* [ruby/reline] Organize special keys escape sequences41deb1a3d9
* [ruby/readline-ext] Remove unnecessary -I$(top_srcdir) when it's an individual gemefaca4a5f4
* [ruby/readline-ext] Check TestRelineAsReadline existancec0a6303168
* [ruby/readline-ext] The ruby/assert.h is adopted by Ruby 2.7 or later106c31fc1b
* Revert "[ruby/readline-ext] Include ruby/assert.h in ruby/ruby.h so that assertions can be there" This reverts commit425b2064d3
. This cherry-pick was a mistake. * [ruby/readline-ext] Use require check instead of DONT_RUN_RELINE_TEST env1df99d1481
* [ruby/readline-ext] Add spec.extensions8c33abb13c
* [ruby/readline-ext] Use rake/extensiokntask to buildb0b5f709bd
* Fix readline build dependency * [ruby/irb] Add test_complete_symboldbbf086c1f
* [ruby/irb] Check doc namespace correctly IRB::InputCompletor::PerfectMatchedProc crashes when doc not found because a variable name was incorrect.889fd4928f
* [ruby/irb] Fix auto indent with closed brace A closed brace in auto-indent shouldn't affect the next brace in the same line, but it behaves like below: p() { } It's a bug.fbe59e344f
* [ruby/irb] Use 0.step instead of (..0).each for Ruby 2.55d628ca40e
* Revert "[ruby/irb] Add test_complete_symbol" This reverts commit3af3431c2c
. * [ruby/irb] fix reserved words and completion for them6184b227ad
* Add test_complete_symbol The previous version of the test method used a symbol, ":abcdefg" to complete but longer symbols that can be completed are defined by other test methods of other libs. * test/irb/test_completion.rb: suppress a warning: unused literal ignored * [ruby/reline] Use IO#write instead of IO#print IO#print always adds a string of $\ automatically.a93119c847
* [ruby/irb] Version 1.2.2a71753f15a
* [ruby/reline] Version 0.1.3ea2b182466
* [ruby/irb] Include easter-egg.rb in gemspec `irb` doesn't run because this file isn't included in the gem.73cda56d25
* [ruby/irb] Version 1.2.3dd56e06df5
* support multi-run test for test_readline.rb * [ruby/irb] `yield` outside method definition is a syntax errordbc7b059c7
* test/readline - allow ENV control of test class creation In ruby/ruby, the tests run on both readline & reline by creating four test classes: ``` TestReadline TestReadlineHistory TestRelineAsReadline TestRelineAsReadlineHistory ``` Reline inports the test files and uses them in its CI. Adding the ENV control allows it to only run the `TestRelineAsReadline` classes. * Omit test_using_quoting_detection_proc_with_multibyte_input temporarily for random order test * support random order test. test_readline: HISTORY should be empty. test_using_quoting_detection_proc: test_using_quoting_detection_proc_with_multibyte_input: Readline.completer_quote_characters= and Readline.completer_word_break_characters= doesn't accept nil, so skip if previous values are nil. * Set Readline.completion_append_character = nil always GNU Readline add a white space when Readline.completion_append_character is not initialized. * Fix a typo [ci skip] * skip test if Reline.completion_proc is nil. Some other tests can set Reline.completion_proc, so if it is nil, simply skip this test. * Reset Reline.point TestRelineAsReadline#test_insert_text expects Readline.point == 0 at the beginning of the test, but a test violate this assumption. * Convert incompatible encoding symbol names * Ignore incompatible convert of symbols * Add workaround for test-bundler failure500526558 (step)
:16:127 ``` Failures: 1) Bundler.setup when Bundler is bundled doesn't blow up Failure/Error: expect(err).to be_empty expected `"fatal: not a git repository (or any of the parent directories): .git\nfatal: not a git repository (o...the parent directories): .git\nfatal: not a git repository (or any of the parent directories): .git".empty?` to return true, got false Commands: $ /home/runner/work/actions/actions/snapshot-master/ruby \ -I/home/runner/work/actions/actions/snapshot-master/lib:/home/runner/work/actions/actions/snapshot-master/spec/bundler \ -rsupport/hax -rsupport/artifice/fail \ /home/runner/work/actions/actions/snapshot-master/libexec/bundle install --retry 0 Resolving dependencies... Using bundler 2.1.4 Bundle complete! 1 Gemfile dependency, 1 gem now installed. Use `bundle info [gemname]` to see where a bundled gem is installed. fatal: not a git repository (or any of the parent directories): .git fatal: not a git repository (or any of the parent directories): .git fatal: not a git repository (or any of the parent directories): .git # $? => 0 $ /home/runner/work/actions/actions/snapshot-master/ruby \ -I/home/runner/work/actions/actions/snapshot-master/lib:/home/runner/work/actions/actions/snapshot-master/spec/bundler \ -rsupport/hax -rsupport/artifice/fail \ /home/runner/work/actions/actions/snapshot-master/libexec/bundle exec ruby -e \ require\ \'bundler\'\;\ Bundler.setup fatal: not a git repository (or any of the parent directories): .git fatal: not a git repository (or any of the parent directories): .git fatal: not a git repository (or any of the parent directories): .git # $? => 0 # ./spec/bundler/runtime/setup_spec.rb:1056:in `block (3 levels) in <top (required)>' # ./spec/bundler/spec_helper.rb:111:in `block (3 levels) in <top (required)>' # ./spec/bundler/spec_helper.rb:111:in `block (2 levels) in <top (required)>' # ./spec/bundler/spec_helper.rb:78:in `block (2 levels) in <top (required)>' make: *** [yes-test-bundler] Error 1 ``` * [ruby/irb] Unnamed groups are not captured when named groups are used0a641a69b0
* [ruby/reline] Work with wrong $/ value correctly962ebf5a1b
* [ruby/irb] Detect multiple lines output simplify The old implementation performance test code: require 'objspace' puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001) /\A.*\Z/ !~ ('abc' * 20_000_000) puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001) and run `time test.rb`: 2.5868 MB 62.226 MB real 0m1.307s user 0m0.452s sys 0m0.797s The new implementation performance test code: require 'objspace' puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001) ('abc' * 20_000_000).include?("\n") puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001) and run `time test.rb`: 2.5861 MB 62.226 MB real 0m0.132s user 0m0.088s sys 0m0.042s40d6610baf
* [ruby/reline] Suppress error in case INPUTRC env is emptybce7e7562b
* [ruby/reline] Add yamatanooroti rendering testf092519525
* [ruby/reline] Rename test suite name of yamatanooroti testb0f32f5de4
* [ruby/reline] Add a comment why rescue yamatanooroti loading error on the test2a8061daec
* [ruby/irb] Suppress crashing when EncodingError has occurred without lineno13572d8cdc
* [ruby/reline] Suppress error when check ambiguous char width in LANG=C623dffdd75
* [ruby/io-console] Enable only interrupt bits on `intr: true`baaf929041
* [ruby/io-console] bump up to 0.5.4 * [ruby/io-console] Update the minimum requirement of Ruby version73e7b6318a
* [ruby/io-console] Filter Ruby engine name rather than just /ruby/ This breaks tests using this path on JRuby because the `jruby` executable turns into `jjruby` after the sub.e5951aa34c
* [ruby/io-console] bump up to 0.5.5 * [ruby/io-console] Prefer keyword arguments5facbfc4c8
* [ruby/io-console] [DOC] Improved about `intr:`82b630cd79
* [ruby/io-console] Just ignore the extension on other than CRuby41b6f09574
* [ruby/io-console] bump up to 0.5.6 Co-authored-by: KOBAYASHI Shuji <shuujii@gmail.com> Co-authored-by: Ben <kanobt61@gmail.com> Co-authored-by: Yusuke Endoh <mame@ruby-lang.org> Co-authored-by: MSP-Greg <MSP-Greg@users.noreply.github.com> Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org> Co-authored-by: Kenta Murata <mrkn@mrkn.jp> Co-authored-by: Lars Kanis <lars@greiz-reinsdorf.de> Co-authored-by: Lars Kanis <kanis@comcard.de> Co-authored-by: Alan Wu <XrXr@users.noreply.github.com> Co-authored-by: Hiroshi SHIBATA <hsbt@ruby-lang.org> Co-authored-by: Nobuhiro IMAI <nov@yo.rim.or.jp> Co-authored-by: Nick Lewis <nick@puppet.com> Co-authored-by: S-H-GAMELINKS <gamelinks007@gmail.com> Co-authored-by: Koichi Sasada <ko1@atdot.net> Co-authored-by: Kazuhiro NISHIYAMA <zn@mbf.nifty.com> Co-authored-by: Charles Oliver Nutter <headius@headius.com>
2231 lines
70 KiB
Ruby
2231 lines
70 KiB
Ruby
require 'reline/kill_ring'
|
|
require 'reline/unicode'
|
|
|
|
require 'tempfile'
|
|
require 'pathname'
|
|
|
|
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)
|
|
|
|
CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/
|
|
OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/
|
|
NON_PRINTING_START = "\1"
|
|
NON_PRINTING_END = "\2"
|
|
WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/
|
|
|
|
def initialize(config, encoding)
|
|
@config = config
|
|
@completion_append_character = ''
|
|
reset_variables(encoding: encoding)
|
|
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 @prompt_proc
|
|
prompt_list = @prompt_proc.(buffer)
|
|
prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
|
|
prompt = prompt_list[@line_index]
|
|
prompt_width = calculate_width(prompt, true)
|
|
[prompt, prompt_width, prompt_list]
|
|
else
|
|
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
|
|
reset_variables(prompt, encoding: encoding)
|
|
@old_trap = Signal.trap('SIGINT') {
|
|
@old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
|
|
raise Interrupt
|
|
}
|
|
Reline::IOGate.set_winch_handler do
|
|
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
|
|
old_screen_size = @screen_size
|
|
@screen_size = Reline::IOGate.get_screen_size
|
|
if old_screen_size.last < @screen_size.last # columns increase
|
|
@rerender_all = true
|
|
rerender
|
|
else
|
|
back = 0
|
|
new_buffer = whole_lines
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
|
|
new_buffer.each_with_index do |line, index|
|
|
prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
|
|
width = prompt_width + calculate_width(line)
|
|
height = calculate_height_by_width(width)
|
|
back += height
|
|
end
|
|
@highest_in_all = back
|
|
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
|
|
@first_line_started_from =
|
|
if @line_index.zero?
|
|
0
|
|
else
|
|
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list)
|
|
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
|
|
@completion_journey_data = nil
|
|
@completion_state = CompletionState::NORMAL
|
|
@perfect_matched = nil
|
|
@menu_info = nil
|
|
@first_prompt = true
|
|
@searching_prompt = nil
|
|
@first_char = true
|
|
@eof = false
|
|
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_list)
|
|
result = 0
|
|
lines.each_with_index { |line, i|
|
|
prompt = ''
|
|
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
|
|
end
|
|
|
|
private def calculate_height_by_width(width)
|
|
width.div(@screen_size.last) + 1
|
|
end
|
|
|
|
private def split_by_width(prompt, str, max_width)
|
|
lines = [String.new(encoding: @encoding)]
|
|
height = 1
|
|
width = 0
|
|
rest = "#{prompt}#{str}".encode(Encoding::UTF_8)
|
|
in_zero_width = false
|
|
rest.scan(WIDTH_SCANNER) do |gc|
|
|
case gc
|
|
when NON_PRINTING_START
|
|
in_zero_width = true
|
|
when NON_PRINTING_END
|
|
in_zero_width = false
|
|
when CSI_REGEXP, OSC_REGEXP
|
|
lines.last << gc
|
|
else
|
|
unless in_zero_width
|
|
mbchar_width = Reline::Unicode.get_mbchar_width(gc)
|
|
if (width += mbchar_width) > max_width
|
|
width = mbchar_width
|
|
lines << nil
|
|
lines << String.new(encoding: @encoding)
|
|
height += 1
|
|
end
|
|
end
|
|
lines.last << gc
|
|
end
|
|
end
|
|
# The cursor moves to next line in first
|
|
if width == max_width
|
|
lines << nil
|
|
lines << String.new(encoding: @encoding)
|
|
height += 1
|
|
end
|
|
[lines, height]
|
|
end
|
|
|
|
private def scroll_down(val)
|
|
if val <= @rest_height
|
|
Reline::IOGate.move_cursor_down(val)
|
|
@rest_height -= val
|
|
else
|
|
Reline::IOGate.move_cursor_down(@rest_height)
|
|
Reline::IOGate.scroll_down(val - @rest_height)
|
|
@rest_height = 0
|
|
end
|
|
end
|
|
|
|
private def move_cursor_up(val)
|
|
if val > 0
|
|
Reline::IOGate.move_cursor_up(val)
|
|
@rest_height += val
|
|
elsif val < 0
|
|
move_cursor_down(-val)
|
|
end
|
|
end
|
|
|
|
private def move_cursor_down(val)
|
|
if val > 0
|
|
Reline::IOGate.move_cursor_down(val)
|
|
@rest_height -= val
|
|
@rest_height = 0 if @rest_height < 0
|
|
elsif val < 0
|
|
move_cursor_up(-val)
|
|
end
|
|
end
|
|
|
|
private def calculate_nearest_cursor
|
|
@cursor_max = calculate_width(line)
|
|
new_cursor = 0
|
|
new_byte_pointer = 0
|
|
height = 1
|
|
max_width = @screen_size.last
|
|
if @config.editing_mode_is?(:vi_command)
|
|
last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
|
|
if last_byte_size > 0
|
|
last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
|
|
last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
|
|
cursor_max = @cursor_max - last_width
|
|
else
|
|
cursor_max = @cursor_max
|
|
end
|
|
else
|
|
cursor_max = @cursor_max
|
|
end
|
|
@line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
|
|
mbchar_width = Reline::Unicode.get_mbchar_width(gc)
|
|
now = new_cursor + mbchar_width
|
|
if now > cursor_max or now > @cursor
|
|
break
|
|
end
|
|
new_cursor += mbchar_width
|
|
if new_cursor > max_width * height
|
|
height += 1
|
|
end
|
|
new_byte_pointer += gc.bytesize
|
|
end
|
|
@started_from = height - 1
|
|
@cursor = new_cursor
|
|
@byte_pointer = new_byte_pointer
|
|
end
|
|
|
|
def rerender
|
|
return if @line.nil?
|
|
if @menu_info
|
|
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)
|
|
@menu_info = nil
|
|
end
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
|
|
if @cleared
|
|
Reline::IOGate.clear_screen
|
|
@cleared = false
|
|
back = 0
|
|
modify_lines(whole_lines).each_with_index do |line, index|
|
|
if @prompt_proc
|
|
pr = prompt_list[index]
|
|
height = render_partial(pr, calculate_width(pr), line, false)
|
|
else
|
|
height = render_partial(prompt, prompt_width, line, false)
|
|
end
|
|
if index < (@buffer_of_lines.size - 1)
|
|
move_cursor_down(height)
|
|
back += height
|
|
end
|
|
end
|
|
move_cursor_up(back)
|
|
move_cursor_down(@first_line_started_from + @started_from)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
return
|
|
end
|
|
new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
|
|
# FIXME: end of logical line sometimes breaks
|
|
if @previous_line_index or new_highest_in_this != @highest_in_this
|
|
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)
|
|
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 = 0
|
|
modify_lines(new_lines).each_with_index do |line, index|
|
|
if @prompt_proc
|
|
prompt = prompt_list[index]
|
|
prompt_width = calculate_width(prompt, true)
|
|
end
|
|
height = render_partial(prompt, prompt_width, line, false)
|
|
if index < (new_lines.size - 1)
|
|
scroll_down(1)
|
|
back += height
|
|
else
|
|
back += height - 1
|
|
end
|
|
end
|
|
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)
|
|
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)
|
|
@previous_line_index = nil
|
|
rendered = true
|
|
elsif @rerender_all
|
|
move_cursor_up(@first_line_started_from + @started_from)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
back = 0
|
|
new_buffer = whole_lines
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
|
|
new_buffer.each_with_index do |line, index|
|
|
prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
|
|
width = prompt_width + calculate_width(line)
|
|
height = calculate_height_by_width(width)
|
|
back += height
|
|
end
|
|
if back > @highest_in_all
|
|
scroll_down(back - 1)
|
|
move_cursor_up(back - 1)
|
|
elsif back < @highest_in_all
|
|
scroll_down(back)
|
|
Reline::IOGate.erase_after_cursor
|
|
(@highest_in_all - back - 1).times do
|
|
scroll_down(1)
|
|
Reline::IOGate.erase_after_cursor
|
|
end
|
|
move_cursor_up(@highest_in_all - 1)
|
|
end
|
|
modify_lines(new_buffer).each_with_index do |line, index|
|
|
if @prompt_proc
|
|
prompt = prompt_list[index]
|
|
prompt_width = calculate_width(prompt, true)
|
|
end
|
|
render_partial(prompt, prompt_width, line, false)
|
|
if index < (new_buffer.size - 1)
|
|
move_cursor_down(1)
|
|
end
|
|
end
|
|
move_cursor_up(back - 1)
|
|
if @prompt_proc
|
|
prompt = prompt_list[@line_index]
|
|
prompt_width = calculate_width(prompt, true)
|
|
end
|
|
@highest_in_all = back
|
|
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
|
|
@first_line_started_from =
|
|
if @line_index.zero?
|
|
0
|
|
else
|
|
calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list)
|
|
end
|
|
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
|
move_cursor_down(@first_line_started_from + @started_from)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
@rerender_all = false
|
|
rendered = true
|
|
end
|
|
line = modify_lines(whole_lines)[@line_index]
|
|
if @is_multiline
|
|
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
|
|
if finished?
|
|
# Always rerender on finish because output_modifier_proc may return a different output.
|
|
render_partial(prompt, prompt_width, line)
|
|
scroll_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
Reline::IOGate.erase_after_cursor
|
|
elsif not rendered
|
|
render_partial(prompt, prompt_width, line)
|
|
end
|
|
else
|
|
render_partial(prompt, prompt_width, line)
|
|
if finished?
|
|
scroll_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
Reline::IOGate.erase_after_cursor
|
|
end
|
|
end
|
|
end
|
|
|
|
private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
|
|
visual_lines, height = split_by_width(prompt, line_to_render.nil? ? '' : line_to_render, @screen_size.last)
|
|
if with_control
|
|
if height > @highest_in_this
|
|
diff = height - @highest_in_this
|
|
scroll_down(diff)
|
|
@highest_in_all += diff
|
|
@highest_in_this = height
|
|
move_cursor_up(diff)
|
|
elsif height < @highest_in_this
|
|
diff = @highest_in_this - height
|
|
@highest_in_all -= diff
|
|
@highest_in_this = height
|
|
end
|
|
move_cursor_up(@started_from)
|
|
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
|
end
|
|
Reline::IOGate.move_cursor_column(0)
|
|
visual_lines.each_with_index do |line, index|
|
|
if line.nil?
|
|
if Reline::IOGate.win? and calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
|
|
# A newline is automatically inserted if a character is rendered at eol on command prompt.
|
|
else
|
|
Reline::IOGate.erase_after_cursor
|
|
move_cursor_down(1)
|
|
Reline::IOGate.move_cursor_column(0)
|
|
end
|
|
next
|
|
end
|
|
@output.write line
|
|
if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
|
|
# A newline is automatically inserted if a character is rendered at eol on command prompt.
|
|
@rest_height -= 1 if @rest_height > 0
|
|
end
|
|
@output.flush
|
|
if @first_prompt
|
|
@first_prompt = false
|
|
@pre_input_hook&.call
|
|
end
|
|
end
|
|
Reline::IOGate.erase_after_cursor
|
|
if with_control
|
|
move_cursor_up(height - 1)
|
|
if finished?
|
|
move_cursor_down(@started_from)
|
|
end
|
|
move_cursor_down(@started_from)
|
|
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
|
end
|
|
height
|
|
end
|
|
|
|
private def modify_lines(before)
|
|
return before if before.nil? || before.empty?
|
|
|
|
if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
|
|
after.lines("\n", chomp: true)
|
|
else
|
|
before
|
|
end
|
|
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
|
|
}
|
|
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
|
|
block.()
|
|
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
|
|
@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
|
|
@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.()
|
|
end
|
|
@waiting_operator_proc = nil
|
|
else
|
|
block.()
|
|
end
|
|
end
|
|
|
|
private def argumentable?(method_obj)
|
|
method_obj and method_obj.parameters.length != 1
|
|
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
|
|
method_obj.(key, arg: @vi_arg)
|
|
end
|
|
else
|
|
method_obj&.(key)
|
|
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
|
|
method_obj.(key, arg: @vi_arg)
|
|
end
|
|
elsif @waiting_proc
|
|
@waiting_proc.(key)
|
|
elsif method_obj
|
|
method_obj.(key)
|
|
else
|
|
ed_insert(key)
|
|
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
|
|
method_obj.(key)
|
|
else
|
|
run_for_operators(key, method_symbol) do
|
|
method_obj.(key)
|
|
end
|
|
end
|
|
@kill_ring.process
|
|
else
|
|
ed_insert(key)
|
|
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)
|
|
if key.char.nil?
|
|
if @first_char
|
|
@line = nil
|
|
end
|
|
finish
|
|
return
|
|
end
|
|
@first_char = false
|
|
completion_occurs = false
|
|
if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
|
|
unless @config.disable_completion
|
|
result = call_completion_proc
|
|
if result.is_a?(Array)
|
|
completion_occurs = true
|
|
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
|
|
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 @is_multiline and @auto_indent_proc
|
|
process_auto_indent
|
|
end
|
|
end
|
|
|
|
def call_completion_proc
|
|
result = retrieve_completion_block(true)
|
|
slice = result[1]
|
|
result = @completion_proc.(slice) if @completion_proc and slice
|
|
Reline.core.instance_variable_set(:@completion_quote_character, nil)
|
|
result
|
|
end
|
|
|
|
private def process_auto_indent
|
|
return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
|
|
if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
|
|
# Fix indent of a line when a newline is inserted to the next
|
|
new_lines = whole_lines(index: @previous_line_index, line: @line)
|
|
new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true)
|
|
md = @line.match(/\A */)
|
|
prev_indent = md[0].count(' ')
|
|
@line = ' ' * new_indent + @line.lstrip
|
|
|
|
new_indent = nil
|
|
result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false)
|
|
if result
|
|
new_indent = result
|
|
end
|
|
if new_indent&.>= 0
|
|
@line = ' ' * new_indent + @line.lstrip
|
|
end
|
|
end
|
|
if @previous_line_index
|
|
new_lines = whole_lines(index: @previous_line_index, line: @line)
|
|
else
|
|
new_lines = whole_lines
|
|
end
|
|
new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
|
|
if new_indent&.>= 0
|
|
md = new_lines[@line_index].match(/\A */)
|
|
prev_indent = md[0].count(' ')
|
|
if @check_new_auto_indent
|
|
@buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
|
|
@cursor = new_indent
|
|
@byte_pointer = new_indent
|
|
else
|
|
@line = ' ' * new_indent + @line.lstrip
|
|
@cursor += new_indent - prev_indent
|
|
@byte_pointer += new_indent - prev_indent
|
|
end
|
|
end
|
|
@check_new_auto_indent = false
|
|
end
|
|
|
|
def retrieve_completion_block(set_completion_quote_character = false)
|
|
word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
|
|
quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
|
|
before = @line.byteslice(0, @byte_pointer)
|
|
rest = nil
|
|
break_pointer = nil
|
|
quote = nil
|
|
closing_quote = nil
|
|
escaped_quote = nil
|
|
i = 0
|
|
while i < @byte_pointer do
|
|
slice = @line.byteslice(i, @byte_pointer - i)
|
|
unless slice.valid_encoding?
|
|
i += 1
|
|
next
|
|
end
|
|
if quote and slice.start_with?(closing_quote)
|
|
quote = nil
|
|
i += 1
|
|
rest = nil
|
|
elsif quote and slice.start_with?(escaped_quote)
|
|
# skip
|
|
i += 2
|
|
elsif slice =~ quote_characters_regexp # find new "
|
|
rest = $'
|
|
quote = $&
|
|
closing_quote = /(?!\\)#{Regexp.escape(quote)}/
|
|
escaped_quote = /\\#{Regexp.escape(quote)}/
|
|
i += 1
|
|
break_pointer = i - 1
|
|
elsif not quote and slice =~ word_break_regexp
|
|
rest = $'
|
|
i += 1
|
|
before = @line.byteslice(i, @byte_pointer - i)
|
|
break_pointer = i
|
|
else
|
|
i += 1
|
|
end
|
|
end
|
|
postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
|
|
if rest
|
|
preposing = @line.byteslice(0, break_pointer)
|
|
target = rest
|
|
if set_completion_quote_character and quote
|
|
Reline.core.instance_variable_set(:@completion_quote_character, quote)
|
|
if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote
|
|
insert_text(quote)
|
|
end
|
|
end
|
|
else
|
|
preposing = ''
|
|
if break_pointer
|
|
preposing = @line.byteslice(0, break_pointer)
|
|
else
|
|
preposing = ''
|
|
end
|
|
target = before
|
|
end
|
|
[preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
|
|
end
|
|
|
|
def confirm_multiline_termination
|
|
temp_buffer = @buffer_of_lines.dup
|
|
if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
|
|
temp_buffer[@previous_line_index] = @line
|
|
else
|
|
temp_buffer[@line_index] = @line
|
|
end
|
|
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
|
|
end
|
|
|
|
def insert_text(text)
|
|
width = calculate_width(text)
|
|
if @cursor == @cursor_max
|
|
@line += text
|
|
else
|
|
@line = byteinsert(@line, @byte_pointer, text)
|
|
end
|
|
@byte_pointer += text.bytesize
|
|
@cursor += width
|
|
@cursor_max += width
|
|
end
|
|
|
|
def delete_text(start = nil, length = nil)
|
|
if start.nil? and length.nil?
|
|
@line&.clear
|
|
@byte_pointer = 0
|
|
@cursor = 0
|
|
@cursor_max = 0
|
|
elsif not start.nil? and not length.nil?
|
|
if @line
|
|
before = @line.byteslice(0, start)
|
|
after = @line.byteslice(start + length, @line.bytesize)
|
|
@line = before + after
|
|
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
|
|
str = @line.byteslice(0, @byte_pointer)
|
|
@cursor = calculate_width(str)
|
|
@cursor_max = calculate_width(@line)
|
|
end
|
|
elsif start.is_a?(Range)
|
|
range = start
|
|
first = range.first
|
|
last = range.last
|
|
last = @line.bytesize - 1 if last > @line.bytesize
|
|
last += @line.bytesize if last < 0
|
|
first += @line.bytesize if first < 0
|
|
range = range.exclude_end? ? first...last : first..last
|
|
@line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
|
|
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
|
|
str = @line.byteslice(0, @byte_pointer)
|
|
@cursor = calculate_width(str)
|
|
@cursor_max = calculate_width(@line)
|
|
else
|
|
@line = @line.byteslice(0, start)
|
|
@byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
|
|
str = @line.byteslice(0, @byte_pointer)
|
|
@cursor = calculate_width(str)
|
|
@cursor_max = calculate_width(@line)
|
|
end
|
|
end
|
|
|
|
def byte_pointer=(val)
|
|
@byte_pointer = val
|
|
str = @line.byteslice(0, @byte_pointer)
|
|
@cursor = calculate_width(str)
|
|
@cursor_max = calculate_width(@line)
|
|
end
|
|
|
|
def whole_lines(index: @line_index, line: @line)
|
|
temp_lines = @buffer_of_lines.dup
|
|
temp_lines[index] = line
|
|
temp_lines
|
|
end
|
|
|
|
def whole_buffer
|
|
if @buffer_of_lines.size == 1 and @line.nil?
|
|
nil
|
|
else
|
|
whole_lines.join("\n")
|
|
end
|
|
end
|
|
|
|
def finished?
|
|
@finished
|
|
end
|
|
|
|
def finish
|
|
@finished = true
|
|
@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)
|
|
if allow_escape_code
|
|
width = 0
|
|
rest = str.encode(Encoding::UTF_8)
|
|
in_zero_width = false
|
|
rest.scan(WIDTH_SCANNER) do |gc|
|
|
case gc
|
|
when NON_PRINTING_START
|
|
in_zero_width = true
|
|
when NON_PRINTING_END
|
|
in_zero_width = false
|
|
when CSI_REGEXP, OSC_REGEXP
|
|
else
|
|
unless in_zero_width
|
|
width += Reline::Unicode.get_mbchar_width(gc)
|
|
end
|
|
end
|
|
end
|
|
width
|
|
else
|
|
str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |w, gc|
|
|
w + Reline::Unicode.get_mbchar_width(gc)
|
|
}
|
|
end
|
|
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
|
|
next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
|
|
cursor_line = @line.byteslice(0, @byte_pointer)
|
|
insert_new_line(cursor_line, next_line)
|
|
@cursor = 0
|
|
@check_new_auto_indent = true
|
|
end
|
|
end
|
|
|
|
private def ed_unassigned(key) end # do nothing
|
|
|
|
private def ed_insert(key)
|
|
if key.instance_of?(String)
|
|
begin
|
|
key.encode(Encoding::UTF_8)
|
|
rescue Encoding::UndefinedConversionError
|
|
return
|
|
end
|
|
width = Reline::Unicode.get_mbchar_width(key)
|
|
if @cursor == @cursor_max
|
|
@line += key
|
|
else
|
|
@line = byteinsert(@line, @byte_pointer, key)
|
|
end
|
|
@byte_pointer += key.bytesize
|
|
@cursor += width
|
|
@cursor_max += width
|
|
else
|
|
begin
|
|
key.chr.encode(Encoding::UTF_8)
|
|
rescue Encoding::UndefinedConversionError
|
|
return
|
|
end
|
|
if @cursor == @cursor_max
|
|
@line += key.chr
|
|
else
|
|
@line = byteinsert(@line, @byte_pointer, key.chr)
|
|
end
|
|
width = Reline::Unicode.get_mbchar_width(key.chr)
|
|
@byte_pointer += 1
|
|
@cursor += width
|
|
@cursor_max += width
|
|
end
|
|
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
|
|
|
|
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 # TODO
|
|
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 search_history(key)
|
|
unless @history_pointer
|
|
if @is_multiline
|
|
@line_backup_in_history = whole_buffer
|
|
else
|
|
@line_backup_in_history = @line
|
|
end
|
|
end
|
|
searcher = generate_searcher
|
|
searcher.resume(key)
|
|
@searching_prompt = "(reverse-i-search)`': "
|
|
@waiting_proc = ->(k) {
|
|
case k
|
|
when "\C-j".ord
|
|
if @history_pointer
|
|
buffer = Reline::HISTORY[@history_pointer]
|
|
else
|
|
buffer = @line_backup_in_history
|
|
end
|
|
if @is_multiline
|
|
@buffer_of_lines = buffer.split("\n")
|
|
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
|
|
@line_index = @buffer_of_lines.size - 1
|
|
@line = @buffer_of_lines.last
|
|
@rerender_all = true
|
|
else
|
|
@line = buffer
|
|
end
|
|
@searching_prompt = nil
|
|
@waiting_proc = nil
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = @byte_pointer = 0
|
|
searcher.resume(-1)
|
|
when "\C-g".ord
|
|
if @is_multiline
|
|
@buffer_of_lines = @line_backup_in_history.split("\n")
|
|
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
|
|
@line_index = @buffer_of_lines.size - 1
|
|
@line = @buffer_of_lines.last
|
|
@rerender_all = true
|
|
else
|
|
@line = @line_backup_in_history
|
|
end
|
|
@history_pointer = nil
|
|
@searching_prompt = nil
|
|
@waiting_proc = nil
|
|
@line_backup_in_history = nil
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = @byte_pointer = 0
|
|
@rerender_all = true
|
|
else
|
|
chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT)
|
|
if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord
|
|
searcher.resume(k)
|
|
else
|
|
if @history_pointer
|
|
line = Reline::HISTORY[@history_pointer]
|
|
else
|
|
line = @line_backup_in_history
|
|
end
|
|
if @is_multiline
|
|
@line_backup_in_history = whole_buffer
|
|
@buffer_of_lines = line.split("\n")
|
|
@buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
|
|
@line_index = @buffer_of_lines.size - 1
|
|
@line = @buffer_of_lines.last
|
|
@rerender_all = true
|
|
else
|
|
@line_backup_in_history = @line
|
|
@line = line
|
|
end
|
|
@searching_prompt = nil
|
|
@waiting_proc = nil
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = @byte_pointer = 0
|
|
searcher.resume(-1)
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
private def ed_search_prev_history(key)
|
|
search_history(key)
|
|
end
|
|
alias_method :reverse_search_history, :ed_search_prev_history
|
|
|
|
private def ed_search_next_history(key)
|
|
search_history(key)
|
|
end
|
|
alias_method :forward_search_history, :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)
|
|
if @is_multiline
|
|
if @config.editing_mode_is?(:vi_command)
|
|
if @line_index < (@buffer_of_lines.size - 1)
|
|
ed_next_history(key) # means cursor down
|
|
else
|
|
# should check confirm_multiline_termination to finish?
|
|
finish
|
|
end
|
|
else
|
|
if @line_index == (@buffer_of_lines.size - 1)
|
|
if confirm_multiline_termination
|
|
finish
|
|
else
|
|
key_newline(key)
|
|
end
|
|
else
|
|
# should check confirm_multiline_termination to finish?
|
|
@previous_line_index = @line_index
|
|
@line_index = @buffer_of_lines.size - 1
|
|
finish
|
|
end
|
|
end
|
|
else
|
|
if @history_pointer
|
|
Reline::HISTORY[@history_pointer] = @line
|
|
@history_pointer = nil
|
|
end
|
|
finish
|
|
end
|
|
end
|
|
|
|
private def em_delete_prev_char(key)
|
|
if @is_multiline and @cursor == 0 and @line_index > 0
|
|
@buffer_of_lines[@line_index] = @line
|
|
@cursor = calculate_width(@buffer_of_lines[@line_index - 1])
|
|
@byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
|
|
@buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
|
|
@line_index -= 1
|
|
@line = @buffer_of_lines[@line_index]
|
|
@cursor_max = calculate_width(@line)
|
|
@rerender_all = true
|
|
elsif @cursor > 0
|
|
byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
|
|
@byte_pointer -= byte_size
|
|
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
|
|
width = Reline::Unicode.get_mbchar_width(mbchar)
|
|
@cursor -= width
|
|
@cursor_max -= width
|
|
end
|
|
end
|
|
alias_method :backward_delete_char, :em_delete_prev_char
|
|
|
|
private def ed_kill_line(key)
|
|
if @line.bytesize > @byte_pointer
|
|
@line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
|
|
@byte_pointer = @line.bytesize
|
|
@cursor = @cursor_max = calculate_width(@line)
|
|
@kill_ring.append(deleted)
|
|
elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
|
|
@cursor = calculate_width(@line)
|
|
@byte_pointer = @line.bytesize
|
|
@line += @buffer_of_lines.delete_at(@line_index + 1)
|
|
@cursor_max = calculate_width(@line)
|
|
@buffer_of_lines[@line_index] = @line
|
|
@rerender_all = true
|
|
@rest_height += 1
|
|
end
|
|
end
|
|
|
|
private def em_kill_line(key)
|
|
if @byte_pointer > 0
|
|
@line, deleted = byteslice!(@line, 0, @byte_pointer)
|
|
@byte_pointer = 0
|
|
@kill_ring.append(deleted, true)
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = 0
|
|
end
|
|
end
|
|
|
|
private def em_delete(key)
|
|
if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
|
|
@line = nil
|
|
if @buffer_of_lines.size > 1
|
|
scroll_down(@highest_in_all - @first_line_started_from)
|
|
end
|
|
Reline::IOGate.move_cursor_column(0)
|
|
@eof = true
|
|
finish
|
|
elsif @byte_pointer < @line.bytesize
|
|
splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
|
|
mbchar = splitted_last.grapheme_clusters.first
|
|
width = Reline::Unicode.get_mbchar_width(mbchar)
|
|
@cursor_max -= width
|
|
@line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
|
|
elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
|
|
@cursor = calculate_width(@line)
|
|
@byte_pointer = @line.bytesize
|
|
@line += @buffer_of_lines.delete_at(@line_index + 1)
|
|
@cursor_max = calculate_width(@line)
|
|
@buffer_of_lines[@line_index] = @line
|
|
@rerender_all = true
|
|
@rest_height += 1
|
|
end
|
|
end
|
|
alias_method :delete_char, :em_delete
|
|
|
|
private def em_delete_or_list(key)
|
|
if @line.empty? or @byte_pointer < @line.bytesize
|
|
em_delete(key)
|
|
else # show completed list
|
|
result = call_completion_proc
|
|
if result.is_a?(Array)
|
|
complete(result, true)
|
|
end
|
|
end
|
|
end
|
|
alias_method :delete_char_or_list, :em_delete_or_list
|
|
|
|
private def em_yank(key)
|
|
yanked = @kill_ring.yank
|
|
if yanked
|
|
@line = byteinsert(@line, @byte_pointer, yanked)
|
|
yanked_width = calculate_width(yanked)
|
|
@cursor += yanked_width
|
|
@cursor_max += yanked_width
|
|
@byte_pointer += yanked.bytesize
|
|
end
|
|
end
|
|
|
|
private def em_yank_pop(key)
|
|
yanked, prev_yank = @kill_ring.yank_pop
|
|
if yanked
|
|
prev_yank_width = calculate_width(prev_yank)
|
|
@cursor -= prev_yank_width
|
|
@cursor_max -= prev_yank_width
|
|
@byte_pointer -= prev_yank.bytesize
|
|
@line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
|
|
@line = byteinsert(@line, @byte_pointer, yanked)
|
|
yanked_width = calculate_width(yanked)
|
|
@cursor += yanked_width
|
|
@cursor_max += yanked_width
|
|
@byte_pointer += yanked.bytesize
|
|
end
|
|
end
|
|
|
|
private def ed_clear_screen(key)
|
|
@cleared = true
|
|
end
|
|
alias_method :clear_screen, :ed_clear_screen
|
|
|
|
private def em_next_word(key)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
|
|
@byte_pointer += byte_size
|
|
@cursor += width
|
|
end
|
|
end
|
|
alias_method :forward_word, :em_next_word
|
|
|
|
private def ed_prev_word(key)
|
|
if @byte_pointer > 0
|
|
byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
end
|
|
end
|
|
alias_method :backward_word, :ed_prev_word
|
|
|
|
private def em_delete_next_word(key)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
|
|
@line, word = byteslice!(@line, @byte_pointer, byte_size)
|
|
@kill_ring.append(word)
|
|
@cursor_max -= width
|
|
end
|
|
end
|
|
|
|
private def ed_delete_prev_word(key)
|
|
if @byte_pointer > 0
|
|
byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
|
|
@line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size)
|
|
@kill_ring.append(word, true)
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
@cursor_max -= width
|
|
end
|
|
end
|
|
|
|
private def ed_transpose_chars(key)
|
|
if @byte_pointer > 0
|
|
if @cursor_max > @cursor
|
|
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
|
|
mbchar = @line.byteslice(@byte_pointer, byte_size)
|
|
width = Reline::Unicode.get_mbchar_width(mbchar)
|
|
@cursor += width
|
|
@byte_pointer += byte_size
|
|
end
|
|
back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
|
|
if (@byte_pointer - back1_byte_size) > 0
|
|
back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size)
|
|
back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size
|
|
@line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size)
|
|
@line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar)
|
|
end
|
|
end
|
|
end
|
|
alias_method :transpose_chars, :ed_transpose_chars
|
|
|
|
private def ed_transpose_words(key)
|
|
left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer)
|
|
before = @line.byteslice(0, left_word_start)
|
|
left_word = @line.byteslice(left_word_start, middle_start - left_word_start)
|
|
middle = @line.byteslice(middle_start, right_word_start - middle_start)
|
|
right_word = @line.byteslice(right_word_start, after_start - right_word_start)
|
|
after = @line.byteslice(after_start, @line.bytesize - after_start)
|
|
return if left_word.empty? or right_word.empty?
|
|
@line = before + right_word + middle + left_word + after
|
|
from_head_to_left_word = before + right_word + middle + left_word
|
|
@byte_pointer = from_head_to_left_word.bytesize
|
|
@cursor = calculate_width(from_head_to_left_word)
|
|
end
|
|
alias_method :transpose_words, :ed_transpose_words
|
|
|
|
private def em_capitol_case(key)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer)
|
|
before = @line.byteslice(0, @byte_pointer)
|
|
after = @line.byteslice((@byte_pointer + byte_size)..-1)
|
|
@line = before + new_str + after
|
|
@byte_pointer += new_str.bytesize
|
|
@cursor += calculate_width(new_str)
|
|
end
|
|
end
|
|
alias_method :capitalize_word, :em_capitol_case
|
|
|
|
private def em_lower_case(key)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
|
|
part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
|
|
mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
|
|
}.join
|
|
rest = @line.byteslice((@byte_pointer + byte_size)..-1)
|
|
@line = @line.byteslice(0, @byte_pointer) + part
|
|
@byte_pointer = @line.bytesize
|
|
@cursor = calculate_width(@line)
|
|
@cursor_max = @cursor + calculate_width(rest)
|
|
@line += rest
|
|
end
|
|
end
|
|
alias_method :downcase_word, :em_lower_case
|
|
|
|
private def em_upper_case(key)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
|
|
part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
|
|
mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
|
|
}.join
|
|
rest = @line.byteslice((@byte_pointer + byte_size)..-1)
|
|
@line = @line.byteslice(0, @byte_pointer) + part
|
|
@byte_pointer = @line.bytesize
|
|
@cursor = calculate_width(@line)
|
|
@cursor_max = @cursor + calculate_width(rest)
|
|
@line += rest
|
|
end
|
|
end
|
|
alias_method :upcase_word, :em_upper_case
|
|
|
|
private def em_kill_region(key)
|
|
if @byte_pointer > 0
|
|
byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer)
|
|
@line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size)
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
@cursor_max -= width
|
|
@kill_ring.append(deleted)
|
|
end
|
|
end
|
|
|
|
private def copy_for_vi(text)
|
|
if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
|
|
@vi_clipboard = text
|
|
end
|
|
end
|
|
|
|
private def vi_insert(key)
|
|
@config.editing_mode = :vi_insert
|
|
end
|
|
|
|
private def vi_add(key)
|
|
@config.editing_mode = :vi_insert
|
|
ed_next_char(key)
|
|
end
|
|
|
|
private def vi_command_mode(key)
|
|
ed_prev_char(key)
|
|
@config.editing_mode = :vi_command
|
|
end
|
|
alias_method :backward_char, :ed_prev_char
|
|
|
|
private def vi_next_word(key, arg: 1)
|
|
if @line.bytesize > @byte_pointer
|
|
byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
|
|
@byte_pointer += byte_size
|
|
@cursor += width
|
|
end
|
|
arg -= 1
|
|
vi_next_word(key, arg: arg) if arg > 0
|
|
end
|
|
|
|
private def vi_prev_word(key, arg: 1)
|
|
if @byte_pointer > 0
|
|
byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer)
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
end
|
|
arg -= 1
|
|
vi_prev_word(key, arg: arg) if arg > 0
|
|
end
|
|
|
|
private def vi_end_word(key, arg: 1)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
@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
|
|
}
|
|
end
|
|
|
|
private def vi_delete_meta(key)
|
|
@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
|
|
}
|
|
end
|
|
|
|
private def vi_yank(key)
|
|
end
|
|
|
|
private def vi_list_or_eof(key)
|
|
if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
|
|
@line = nil
|
|
if @buffer_of_lines.size > 1
|
|
scroll_down(@highest_in_all - @first_line_started_from)
|
|
end
|
|
Reline::IOGate.move_cursor_column(0)
|
|
@eof = true
|
|
finish
|
|
else
|
|
ed_newline(key)
|
|
end
|
|
end
|
|
alias_method :vi_end_of_transmission, :vi_list_or_eof
|
|
alias_method :vi_eof_maybe, :vi_list_or_eof
|
|
|
|
private def ed_delete_next_char(key, arg: 1)
|
|
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
|
|
unless @line.empty? || byte_size == 0
|
|
@line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
|
|
copy_for_vi(mbchar)
|
|
width = Reline::Unicode.get_mbchar_width(mbchar)
|
|
@cursor_max -= width
|
|
if @cursor > 0 and @cursor >= @cursor_max
|
|
@byte_pointer -= byte_size
|
|
@cursor -= width
|
|
end
|
|
end
|
|
arg -= 1
|
|
ed_delete_next_char(key, arg: arg) if arg > 0
|
|
end
|
|
|
|
private def vi_to_history_line(key)
|
|
if Reline::HISTORY.empty?
|
|
return
|
|
end
|
|
if @history_pointer.nil?
|
|
@history_pointer = 0
|
|
@line_backup_in_history = @line
|
|
@line = Reline::HISTORY[@history_pointer]
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = 0
|
|
@byte_pointer = 0
|
|
elsif @history_pointer.zero?
|
|
return
|
|
else
|
|
Reline::HISTORY[@history_pointer] = @line
|
|
@history_pointer = 0
|
|
@line = Reline::HISTORY[@history_pointer]
|
|
@cursor_max = calculate_width(@line)
|
|
@cursor = 0
|
|
@byte_pointer = 0
|
|
end
|
|
end
|
|
|
|
private def vi_histedit(key)
|
|
path = Tempfile.open { |fp|
|
|
fp.write @line
|
|
fp.path
|
|
}
|
|
system("#{ENV['EDITOR']} #{path}")
|
|
@line = Pathname.new(path).read
|
|
finish
|
|
end
|
|
|
|
private def vi_paste_prev(key, arg: 1)
|
|
if @vi_clipboard.size > 0
|
|
@line = byteinsert(@line, @byte_pointer, @vi_clipboard)
|
|
@cursor_max += calculate_width(@vi_clipboard)
|
|
cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
|
|
@cursor += calculate_width(cursor_point)
|
|
@byte_pointer += cursor_point.bytesize
|
|
end
|
|
arg -= 1
|
|
vi_paste_prev(key, arg: arg) if arg > 0
|
|
end
|
|
|
|
private def vi_paste_next(key, arg: 1)
|
|
if @vi_clipboard.size > 0
|
|
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
|
|
@line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard)
|
|
@cursor_max += calculate_width(@vi_clipboard)
|
|
@cursor += calculate_width(@vi_clipboard)
|
|
@byte_pointer += @vi_clipboard.bytesize
|
|
end
|
|
arg -= 1
|
|
vi_paste_next(key, arg: arg) if arg > 0
|
|
end
|
|
|
|
private def ed_argument_digit(key)
|
|
if @vi_arg.nil?
|
|
unless key.chr.to_i.zero?
|
|
@vi_arg = key.chr.to_i
|
|
end
|
|
else
|
|
@vi_arg = @vi_arg * 10 + key.chr.to_i
|
|
end
|
|
end
|
|
|
|
private def vi_to_column(key, arg: 0)
|
|
@byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc|
|
|
# total has [byte_size, cursor]
|
|
mbchar_width = Reline::Unicode.get_mbchar_width(gc)
|
|
if (total.last + mbchar_width) >= arg
|
|
break total
|
|
elsif (total.last + mbchar_width) >= @cursor_max
|
|
break total
|
|
else
|
|
total = [total.first + gc.bytesize, total.last + mbchar_width]
|
|
total
|
|
end
|
|
}
|
|
end
|
|
|
|
private def vi_replace_char(key, arg: 1)
|
|
@waiting_proc = ->(k) {
|
|
if arg == 1
|
|
byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
|
|
before = @line.byteslice(0, @byte_pointer)
|
|
remaining_point = @byte_pointer + byte_size
|
|
after = @line.byteslice(remaining_point, @line.size - remaining_point)
|
|
@line = before + k.chr + after
|
|
@cursor_max = calculate_width(@line)
|
|
@waiting_proc = nil
|
|
elsif arg > 1
|
|
byte_size = 0
|
|
arg.times do
|
|
byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size)
|
|
end
|
|
before = @line.byteslice(0, @byte_pointer)
|
|
remaining_point = @byte_pointer + byte_size
|
|
after = @line.byteslice(remaining_point, @line.size - remaining_point)
|
|
replaced = k.chr * arg
|
|
@line = before + replaced + after
|
|
@byte_pointer += replaced.bytesize
|
|
@cursor += calculate_width(replaced)
|
|
@cursor_max = calculate_width(@line)
|
|
@waiting_proc = nil
|
|
end
|
|
}
|
|
end
|
|
|
|
private def vi_next_char(key, arg: 1)
|
|
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
|
|
end
|
|
|
|
private def vi_to_next_char(key, arg: 1)
|
|
@waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
|
|
end
|
|
|
|
private def search_next_char(key, arg, need_prev_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(@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
|
|
@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)
|
|
new_pointer = [@byte_pointer, @line_index]
|
|
@previous_line_index = @line_index
|
|
@byte_pointer, @line_index = @mark_pointer
|
|
@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
|