Class: Reline::LineEditor

Inherits:
Object
  • Object
show all
Defined in:
lib/reline/line_editor.rb

Defined Under Namespace

Modules: CompletionState Classes: CompletionJourneyData, MenuInfo

Constant Summary collapse

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
}
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)/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ LineEditor

Returns a new instance of LineEditor.



58
59
60
61
# File 'lib/reline/line_editor.rb', line 58

def initialize(config)
  @config = config
  reset_variables
end

Instance Attribute Details

#auto_indent_procObject

Returns the value of attribute auto_indent_proc.



15
16
17
# File 'lib/reline/line_editor.rb', line 15

def auto_indent_proc
  @auto_indent_proc
end

#byte_pointerObject

Returns the value of attribute byte_pointer.



10
11
12
# File 'lib/reline/line_editor.rb', line 10

def byte_pointer
  @byte_pointer
end

#completion_procObject

Returns the value of attribute completion_proc.



12
13
14
# File 'lib/reline/line_editor.rb', line 12

def completion_proc
  @completion_proc
end

#confirm_multiline_termination_procObject

Returns the value of attribute confirm_multiline_termination_proc.



11
12
13
# File 'lib/reline/line_editor.rb', line 11

def confirm_multiline_termination_proc
  @confirm_multiline_termination_proc
end

#dig_perfect_match_procObject

Returns the value of attribute dig_perfect_match_proc.



17
18
19
# File 'lib/reline/line_editor.rb', line 17

def dig_perfect_match_proc
  @dig_perfect_match_proc
end

#lineObject (readonly)

TODO: undo



9
10
11
# File 'lib/reline/line_editor.rb', line 9

def line
  @line
end

#output=(value) ⇒ Object (writeonly)

Sets the attribute output

Parameters:

  • value

    the value to set the attribute output to.



18
19
20
# File 'lib/reline/line_editor.rb', line 18

def output=(value)
  @output = value
end

#output_modifier_procObject

Returns the value of attribute output_modifier_proc.



13
14
15
# File 'lib/reline/line_editor.rb', line 13

def output_modifier_proc
  @output_modifier_proc
end

#pre_input_hookObject

Returns the value of attribute pre_input_hook.



16
17
18
# File 'lib/reline/line_editor.rb', line 16

def pre_input_hook
  @pre_input_hook
end

#prompt_procObject

Returns the value of attribute prompt_proc.



14
15
16
# File 'lib/reline/line_editor.rb', line 14

def prompt_proc
  @prompt_proc
end

Instance Method Details

#confirm_multiline_terminationObject



835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/reline/line_editor.rb', line 835

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
  if temp_buffer.any?{ |l| l.chomp != '' }
    @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
  else
    false
  end
end

#delete_text(start = nil, length = nil) ⇒ Object



861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
# File 'lib/reline/line_editor.rb', line 861

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

#editing_modeObject



507
508
509
# File 'lib/reline/line_editor.rb', line 507

def editing_mode
  @config.editing_mode
end

#eof?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/reline/line_editor.rb', line 78

def eof?
  @eof
end

#finalizeObject



74
75
76
# File 'lib/reline/line_editor.rb', line 74

def finalize
  Signal.trap('SIGINT', @old_trap)
end

#finishObject



924
925
926
927
# File 'lib/reline/line_editor.rb', line 924

def finish
  @finished = true
  @config.reset
end

#finished?Boolean

Returns:

  • (Boolean)


920
921
922
# File 'lib/reline/line_editor.rb', line 920

def finished?
  @finished
end

#input_key(key) ⇒ Object



734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
# File 'lib/reline/line_editor.rb', line 734

def input_key(key)
  if key.nil? or 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
    result = retrieve_completion_block
    slice = result[1]
    result = @completion_proc.(slice) if @completion_proc and slice
    if result.is_a?(Array)
      completion_occurs = true
      complete(result)
    end
  elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
    result = retrieve_completion_block
    slice = result[1]
    result = @completion_proc.(slice) if @completion_proc and slice
    if result.is_a?(Array)
      completion_occurs = true
      move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
    end
  elsif Symbol === key.char and respond_to?(key.char, true)
    process_key(key.char, key.char)
  else
    normal_char(key)
  end
  unless completion_occurs
    @completion_state = CompletionState::NORMAL
  end
  if @is_multiline and @auto_indent_proc
    process_auto_indent
  end
end

#insert_text(text) ⇒ Object



849
850
851
852
853
854
855
856
857
858
859
# File 'lib/reline/line_editor.rb', line 849

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

#multiline_offObject



128
129
130
# File 'lib/reline/line_editor.rb', line 128

def multiline_off
  @is_multiline = false
end

#multiline_onObject



124
125
126
# File 'lib/reline/line_editor.rb', line 124

def multiline_on
  @is_multiline = true
end

#rerenderObject

TODO: support physical and logical lines



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/reline/line_editor.rb', line 244

def rerender # TODO: support physical and logical lines
  return if @line.nil?
  if @menu_info
    scroll_down(@highest_in_all - @first_line_started_from)
    @rerender_all = true
    @menu_info.list.each do |item|
      Reline::IOGate.move_cursor_column(0)
      @output.print item
      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
  if @vi_arg
    prompt = "(arg: #{@vi_arg}) "
    prompt_width = calculate_width(prompt)
  elsif @searching_prompt
    prompt = @searching_prompt
    prompt_width = calculate_width(prompt)
  else
    prompt = @prompt
    prompt_width = calculate_width(prompt, true)
  end
  if @cleared
    Reline::IOGate.clear_screen
    @cleared = false
    back = 0
    prompt_list = nil
    if @prompt_proc
      prompt_list = @prompt_proc.(whole_lines)
      prompt = prompt_list[@line_index]
      prompt_width = calculate_width(prompt, true)
    end
    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_list = nil
    if @prompt_proc
      prompt_list = @prompt_proc.(new_lines)
      prompt = prompt_list[@line_index]
      prompt_width = calculate_width(prompt, true)
    end
    all_height = new_lines.inject(0) { |result, line|
      result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO 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
        @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
          result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO 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_list = nil
    if @prompt_proc
      prompt_list = @prompt_proc.(new_buffer)
      prompt = prompt_list[@line_index]
      prompt_width = calculate_width(prompt, true)
    end
    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
        new_buffer[0..(@line_index - 1)].inject(0) { |result, line|
          result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO 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_list = nil
    if @prompt_proc
      prompt_list = @prompt_proc.(whole_lines)
      prompt = prompt_list[@line_index]
      prompt_width = calculate_width(prompt, true)
    end
    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

#reset(prompt = '', encoding = Encoding.default_external) ⇒ Object



63
64
65
66
67
68
69
70
71
72
# File 'lib/reline/line_editor.rb', line 63

def reset(prompt = '', encoding = Encoding.default_external)
  @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)
  @old_trap = Signal.trap('SIGINT') {
    scroll_down(@highest_in_all - @first_line_started_from)
    Reline::IOGate.move_cursor_column(0)
    @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
  }
end

#reset_lineObject



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/reline/line_editor.rb', line 106

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

#reset_variables(prompt = '', encoding = Encoding.default_external) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/reline/line_editor.rb', line 82

def reset_variables(prompt = '', encoding = Encoding.default_external)
  @prompt = prompt
  @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

#retrieve_completion_blockObject



797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
# File 'lib/reline/line_editor.rb', line 797

def retrieve_completion_block
  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
  i = 0
  while i < @byte_pointer do
    slice = @line.byteslice(i, @byte_pointer - i)
    if quote and slice.start_with?(/(?!\\)#{Regexp.escape(quote)}/) # closing "
      quote = nil
      i += 1
    elsif quote and slice.start_with?(/\\#{Regexp.escape(quote)}/) # escaped \"
      # skip
      i += 2
    elsif slice =~ quote_characters_regexp # find new "
      quote = $&
      i += 1
    elsif not quote and slice =~ word_break_regexp
      rest = $'
      i += 1
      break_pointer = i
    else
      i += 1
    end
  end
  if rest
    preposing = @line.byteslice(0, break_pointer)
    target = rest
  else
    preposing = ''
    target = before
  end
  postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
  [preposing, target, postposing]
end

#whole_bufferObject



912
913
914
915
916
917
918
# File 'lib/reline/line_editor.rb', line 912

def whole_buffer
  if @buffer_of_lines.size == 1 and @line.nil?
    nil
  else
    whole_lines.join("\n")
  end
end

#whole_lines(index: @line_index, line: @line) ⇒ Object



906
907
908
909
910
# File 'lib/reline/line_editor.rb', line 906

def whole_lines(index: @line_index, line: @line)
  temp_lines = @buffer_of_lines.dup
  temp_lines[index] = line
  temp_lines
end