reline 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,138 +7,99 @@ class Reline::KeyStroke
7
7
  @config = config
8
8
  end
9
9
 
10
- def compress_meta_key(ary)
11
- return ary unless @config.convert_meta
12
- ary.inject([]) { |result, key|
13
- if result.size > 0 and result.last == "\e".ord
14
- result[result.size - 1] = Reline::Key.new(key, key | 0b10000000, true)
15
- else
16
- result << key
17
- end
18
- result
19
- }
20
- end
10
+ # Input exactly matches to a key sequence
11
+ MATCHING = :matching
12
+ # Input partially matches to a key sequence
13
+ MATCHED = :matched
14
+ # Input matches to a key sequence and the key sequence is a prefix of another key sequence
15
+ MATCHING_MATCHED = :matching_matched
16
+ # Input does not match to any key sequence
17
+ UNMATCHED = :unmatched
21
18
 
22
- def start_with?(me, other)
23
- compressed_me = compress_meta_key(me)
24
- compressed_other = compress_meta_key(other)
25
- i = 0
26
- loop do
27
- my_c = compressed_me[i]
28
- other_c = compressed_other[i]
29
- other_is_last = (i + 1) == compressed_other.size
30
- me_is_last = (i + 1) == compressed_me.size
31
- if my_c != other_c
32
- if other_c == "\e".ord and other_is_last and my_c.is_a?(Reline::Key) and my_c.with_meta
33
- return true
34
- else
35
- return false
36
- end
37
- elsif other_is_last
38
- return true
39
- elsif me_is_last
40
- return false
41
- end
42
- i += 1
43
- end
44
- end
19
+ def match_status(input)
20
+ matching = key_mapping.matching?(input)
21
+ matched = key_mapping.get(input)
45
22
 
46
- def equal?(me, other)
47
- case me
48
- when Array
49
- compressed_me = compress_meta_key(me)
50
- compressed_other = compress_meta_key(other)
51
- compressed_me.size == compressed_other.size and [compressed_me, compressed_other].transpose.all?{ |i| equal?(i[0], i[1]) }
52
- when Integer
53
- if other.is_a?(Reline::Key)
54
- if other.combined_char == "\e".ord
55
- false
56
- else
57
- other.combined_char == me
58
- end
59
- else
60
- me == other
61
- end
62
- when Reline::Key
63
- if other.is_a?(Integer)
64
- me.combined_char == other
65
- else
66
- me == other
67
- end
68
- end
69
- end
23
+ # FIXME: Workaround for single byte. remove this after MAPPING is merged into KeyActor.
24
+ matched ||= input.size == 1
25
+ matching ||= input == [ESC_BYTE]
70
26
 
71
- def match_status(input)
72
- key_mapping.keys.select { |lhs|
73
- start_with?(lhs, input)
74
- }.tap { |it|
75
- return :matched if it.size == 1 && equal?(it[0], input)
76
- return :matching if it.size == 1 && !equal?(it[0], input)
77
- return :matched if it.max_by(&:size)&.size&.< input.size
78
- return :matching if it.size > 1
79
- }
80
- if key_mapping.keys.any? { |lhs| start_with?(input, lhs) }
81
- :matched
27
+ if matching && matched
28
+ MATCHING_MATCHED
29
+ elsif matching
30
+ MATCHING
31
+ elsif matched
32
+ MATCHED
33
+ elsif input[0] == ESC_BYTE
34
+ match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command))
35
+ elsif input.size == 1
36
+ MATCHED
82
37
  else
83
- match_unknown_escape_sequence(input).first
38
+ UNMATCHED
84
39
  end
85
40
  end
86
41
 
87
42
  def expand(input)
88
- lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last
89
- unless lhs
90
- status, size = match_unknown_escape_sequence(input)
91
- case status
92
- when :matched
93
- return [:ed_unassigned] + expand(input.drop(size))
94
- when :matching
95
- return [:ed_unassigned]
96
- else
97
- return input
98
- end
43
+ matched_bytes = nil
44
+ (1..input.size).each do |i|
45
+ bytes = input.take(i)
46
+ status = match_status(bytes)
47
+ matched_bytes = bytes if status == MATCHED || status == MATCHING_MATCHED
99
48
  end
100
- rhs = key_mapping[lhs]
49
+ return [[], []] unless matched_bytes
101
50
 
102
- case rhs
103
- when String
104
- rhs_bytes = rhs.bytes
105
- expand(expand(rhs_bytes) + expand(input.drop(lhs.size)))
106
- when Symbol
107
- [rhs] + expand(input.drop(lhs.size))
108
- when Array
109
- rhs
51
+ func = key_mapping.get(matched_bytes)
52
+ if func.is_a?(Array)
53
+ keys = func.map { |c| Reline::Key.new(c, c, false) }
54
+ elsif func
55
+ keys = [Reline::Key.new(func, func, false)]
56
+ elsif matched_bytes.size == 1
57
+ keys = [Reline::Key.new(matched_bytes.first, matched_bytes.first, false)]
58
+ elsif matched_bytes.size == 2 && matched_bytes[0] == ESC_BYTE
59
+ keys = [Reline::Key.new(matched_bytes[1], matched_bytes[1] | 0b10000000, true)]
60
+ else
61
+ keys = []
110
62
  end
63
+
64
+ [keys, input.drop(matched_bytes.size)]
111
65
  end
112
66
 
113
67
  private
114
68
 
115
69
  # returns match status of CSI/SS3 sequence and matched length
116
- def match_unknown_escape_sequence(input)
70
+ def match_unknown_escape_sequence(input, vi_mode: false)
117
71
  idx = 0
118
- return [:unmatched, nil] unless input[idx] == ESC_BYTE
72
+ return UNMATCHED unless input[idx] == ESC_BYTE
119
73
  idx += 1
120
74
  idx += 1 if input[idx] == ESC_BYTE
121
75
 
122
76
  case input[idx]
123
77
  when nil
124
- return [:matching, nil]
78
+ if idx == 1 # `ESC`
79
+ return MATCHING_MATCHED
80
+ else # `ESC ESC`
81
+ return MATCHING
82
+ end
125
83
  when 91 # == '['.ord
126
- # CSI sequence
84
+ # CSI sequence `ESC [ ... char`
127
85
  idx += 1
128
86
  idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx])
129
87
  idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx])
130
- input[idx] ? [:matched, idx + 1] : [:matching, nil]
131
88
  when 79 # == 'O'.ord
132
- # SS3 sequence
133
- input[idx + 1] ? [:matched, idx + 2] : [:matching, nil]
89
+ # SS3 sequence `ESC O char`
90
+ idx += 1
134
91
  else
135
- if idx == 1
136
- # `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence`
137
- [:unmatched, nil]
138
- else
139
- # `ESC ESC char`
140
- [:matched, idx + 1]
141
- end
92
+ # `ESC char` or `ESC ESC char`
93
+ return UNMATCHED if vi_mode
94
+ end
95
+
96
+ case input.size
97
+ when idx
98
+ MATCHING
99
+ when idx + 1
100
+ MATCHED
101
+ else
102
+ UNMATCHED
142
103
  end
143
104
  end
144
105
 
@@ -45,6 +45,7 @@ class Reline::LineEditor
45
45
  RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true)
46
46
 
47
47
  CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer)
48
+ NullActionState = [nil, nil].freeze
48
49
 
49
50
  class MenuInfo
50
51
  attr_reader :list
@@ -237,7 +238,6 @@ class Reline::LineEditor
237
238
  @perfect_matched = nil
238
239
  @menu_info = nil
239
240
  @searching_prompt = nil
240
- @first_char = true
241
241
  @just_cursor_moving = false
242
242
  @eof = false
243
243
  @continuous_insertion_buffer = String.new(encoding: @encoding)
@@ -250,8 +250,11 @@ class Reline::LineEditor
250
250
  @resized = false
251
251
  @cache = {}
252
252
  @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
253
- @past_lines = []
253
+ @input_lines = [[[""], 0, 0]]
254
+ @input_lines_position = 0
254
255
  @undoing = false
256
+ @prev_action_state = NullActionState
257
+ @next_action_state = NullActionState
255
258
  reset_line
256
259
  end
257
260
 
@@ -411,7 +414,7 @@ class Reline::LineEditor
411
414
  # do nothing
412
415
  elsif level == :blank
413
416
  Reline::IOGate.move_cursor_column base_x
414
- @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}"
417
+ @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
415
418
  else
416
419
  x, w, content = new_items[level]
417
420
  cover_begin = base_x != 0 && new_levels[base_x - 1] == level
@@ -421,7 +424,7 @@ class Reline::LineEditor
421
424
  content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
422
425
  end
423
426
  Reline::IOGate.move_cursor_column x + pos
424
- @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}"
427
+ @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
425
428
  end
426
429
  base_x += width
427
430
  end
@@ -683,10 +686,8 @@ class Reline::LineEditor
683
686
  @trap_key.each do |t|
684
687
  @config.add_oneshot_key_binding(t, @name)
685
688
  end
686
- elsif @trap_key.is_a?(Array)
689
+ else
687
690
  @config.add_oneshot_key_binding(@trap_key, @name)
688
- elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key)
689
- @config.add_oneshot_key_binding([@trap_key], @name)
690
691
  end
691
692
  end
692
693
  dialog_render_info
@@ -1079,17 +1080,7 @@ class Reline::LineEditor
1079
1080
  else # single byte
1080
1081
  return if key.char >= 128 # maybe, first byte of multi byte
1081
1082
  method_symbol = @config.editing_mode.get_method(key.combined_char)
1082
- if key.with_meta and method_symbol == :ed_unassigned
1083
- if @config.editing_mode_is?(:vi_command, :vi_insert)
1084
- # split ESC + key in vi mode
1085
- method_symbol = @config.editing_mode.get_method("\e".ord)
1086
- process_key("\e".ord, method_symbol)
1087
- method_symbol = @config.editing_mode.get_method(key.char)
1088
- process_key(key.char, method_symbol)
1089
- end
1090
- else
1091
- process_key(key.combined_char, method_symbol)
1092
- end
1083
+ process_key(key.combined_char, method_symbol)
1093
1084
  @multibyte_buffer.clear
1094
1085
  end
1095
1086
  if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
@@ -1118,13 +1109,10 @@ class Reline::LineEditor
1118
1109
  end
1119
1110
  if key.char.nil?
1120
1111
  process_insert(force: true)
1121
- if @first_char
1122
- @eof = true
1123
- end
1112
+ @eof = buffer_empty?
1124
1113
  finish
1125
1114
  return
1126
1115
  end
1127
- @first_char = false
1128
1116
  @completion_occurs = false
1129
1117
 
1130
1118
  if key.char.is_a?(Symbol)
@@ -1132,12 +1120,15 @@ class Reline::LineEditor
1132
1120
  else
1133
1121
  normal_char(key)
1134
1122
  end
1123
+
1124
+ @prev_action_state, @next_action_state = @next_action_state, NullActionState
1125
+
1135
1126
  unless @completion_occurs
1136
1127
  @completion_state = CompletionState::NORMAL
1137
1128
  @completion_journey_state = nil
1138
1129
  end
1139
1130
 
1140
- push_past_lines unless @undoing
1131
+ push_input_lines unless @undoing
1141
1132
  @undoing = false
1142
1133
 
1143
1134
  if @in_pasting
@@ -1156,21 +1147,24 @@ class Reline::LineEditor
1156
1147
 
1157
1148
  def save_old_buffer
1158
1149
  @old_buffer_of_lines = @buffer_of_lines.dup
1159
- @old_byte_pointer = @byte_pointer.dup
1160
- @old_line_index = @line_index.dup
1161
1150
  end
1162
1151
 
1163
- def push_past_lines
1164
- if @old_buffer_of_lines != @buffer_of_lines
1165
- @past_lines.push([@old_buffer_of_lines, @old_byte_pointer, @old_line_index])
1152
+ def push_input_lines
1153
+ if @old_buffer_of_lines == @buffer_of_lines
1154
+ @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index]
1155
+ else
1156
+ @input_lines = @input_lines[0..@input_lines_position]
1157
+ @input_lines_position += 1
1158
+ @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index])
1166
1159
  end
1167
- trim_past_lines
1160
+ trim_input_lines
1168
1161
  end
1169
1162
 
1170
- MAX_PAST_LINES = 100
1171
- def trim_past_lines
1172
- if @past_lines.size > MAX_PAST_LINES
1173
- @past_lines.shift
1163
+ MAX_INPUT_LINES = 100
1164
+ def trim_input_lines
1165
+ if @input_lines.size > MAX_INPUT_LINES
1166
+ @input_lines.shift
1167
+ @input_lines_position -= 1
1174
1168
  end
1175
1169
  end
1176
1170
 
@@ -1352,7 +1346,7 @@ class Reline::LineEditor
1352
1346
  @buffer_of_lines[@line_index, 1] = lines
1353
1347
  @line_index += lines.size - 1
1354
1348
  @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
1355
- push_past_lines
1349
+ push_input_lines
1356
1350
  end
1357
1351
 
1358
1352
  def insert_text(text)
@@ -1411,6 +1405,10 @@ class Reline::LineEditor
1411
1405
  whole_lines.join("\n")
1412
1406
  end
1413
1407
 
1408
+ private def buffer_empty?
1409
+ current_line.empty? and @buffer_of_lines.size == 1
1410
+ end
1411
+
1414
1412
  def finished?
1415
1413
  @finished
1416
1414
  end
@@ -1759,29 +1757,31 @@ class Reline::LineEditor
1759
1757
  end
1760
1758
 
1761
1759
  private def ed_search_prev_history(key, arg: 1)
1762
- substr = current_line.byteslice(0, @byte_pointer)
1760
+ substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer)
1763
1761
  return if @history_pointer == 0
1764
1762
  return if @history_pointer.nil? && substr.empty? && !current_line.empty?
1765
1763
 
1766
1764
  history_range = 0...(@history_pointer || Reline::HISTORY.size)
1767
1765
  h_pointer, line_index = search_history(substr, history_range.reverse_each)
1768
1766
  return unless h_pointer
1769
- move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
1767
+ move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
1770
1768
  arg -= 1
1769
+ set_next_action_state(:search_history, :empty) if substr.empty?
1771
1770
  ed_search_prev_history(key, arg: arg) if arg > 0
1772
1771
  end
1773
1772
  alias_method :history_search_backward, :ed_search_prev_history
1774
1773
 
1775
1774
  private def ed_search_next_history(key, arg: 1)
1776
- substr = current_line.byteslice(0, @byte_pointer)
1775
+ substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer)
1777
1776
  return if @history_pointer.nil?
1778
1777
 
1779
1778
  history_range = @history_pointer + 1...Reline::HISTORY.size
1780
1779
  h_pointer, line_index = search_history(substr, history_range)
1781
1780
  return if h_pointer.nil? and not substr.empty?
1782
1781
 
1783
- move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
1782
+ move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
1784
1783
  arg -= 1
1784
+ set_next_action_state(:search_history, :empty) if substr.empty?
1785
1785
  ed_search_next_history(key, arg: arg) if arg > 0
1786
1786
  end
1787
1787
  alias_method :history_search_forward, :ed_search_next_history
@@ -1937,7 +1937,7 @@ class Reline::LineEditor
1937
1937
  alias_method :kill_whole_line, :em_kill_line
1938
1938
 
1939
1939
  private def em_delete(key)
1940
- if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord
1940
+ if buffer_empty? and key == "\C-d".ord
1941
1941
  @eof = true
1942
1942
  finish
1943
1943
  elsif @byte_pointer < current_line.bytesize
@@ -2285,8 +2285,7 @@ class Reline::LineEditor
2285
2285
  end
2286
2286
 
2287
2287
  private def vi_list_or_eof(key)
2288
- if current_line.empty? and @buffer_of_lines.size == 1
2289
- set_current_line('', 0)
2288
+ if buffer_empty?
2290
2289
  @eof = true
2291
2290
  finish
2292
2291
  else
@@ -2529,13 +2528,30 @@ class Reline::LineEditor
2529
2528
  end
2530
2529
 
2531
2530
  private def undo(_key)
2532
- return if @past_lines.empty?
2531
+ @undoing = true
2532
+
2533
+ return if @input_lines_position <= 0
2534
+
2535
+ @input_lines_position -= 1
2536
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2537
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
2538
+ end
2533
2539
 
2540
+ private def redo(_key)
2534
2541
  @undoing = true
2535
2542
 
2536
- target_lines, target_cursor_x, target_cursor_y = @past_lines.last
2537
- set_current_lines(target_lines, target_cursor_x, target_cursor_y)
2543
+ return if @input_lines_position >= @input_lines.size - 1
2544
+
2545
+ @input_lines_position += 1
2546
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2547
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
2548
+ end
2549
+
2550
+ private def prev_action_state_value(type)
2551
+ @prev_action_state[0] == type ? @prev_action_state[1] : nil
2552
+ end
2538
2553
 
2539
- @past_lines.pop
2554
+ private def set_next_action_state(type, value)
2555
+ @next_action_state = [type, value]
2540
2556
  end
2541
2557
  end
@@ -1,4 +1,7 @@
1
1
  begin
2
+ # Ignore warning `Add fiddle to your Gemfile or gemspec` in Ruby 3.4.
3
+ # terminfo.rb and ansi.rb supports fiddle unavailable environment.
4
+ verbose, $VERBOSE = $VERBOSE, nil
2
5
  require 'fiddle'
3
6
  require 'fiddle/import'
4
7
  rescue LoadError
@@ -7,6 +10,8 @@ rescue LoadError
7
10
  false
8
11
  end
9
12
  end
13
+ ensure
14
+ $VERBOSE = verbose
10
15
  end
11
16
 
12
17
  module Reline::Terminfo
@@ -1,3 +1,3 @@
1
1
  module Reline
2
- VERSION = '0.5.7'
2
+ VERSION = '0.5.9'
3
3
  end