Skip to content

Commit 4ab72f9

Browse files
authored
Implement the undo command (#701)
* Refactor send * Implement the undo command * Fix @past_lines initialization * Improve assertion * Hide to save buffer in insert_pasted_text * Replace @using_delete_command with @undoing * Refactor `@past_lines`
1 parent 21891c4 commit 4ab72f9

File tree

4 files changed

+138
-6
lines changed

4 files changed

+138
-6
lines changed

lib/reline/key_actor/emacs.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
6363
# 30 ^^
6464
:ed_unassigned,
6565
# 31 ^_
66-
:ed_unassigned,
66+
:undo,
6767
# 32 SPACE
6868
:ed_insert,
6969
# 33 !

lib/reline/line_editor.rb

+56-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
require 'tempfile'
55

66
class Reline::LineEditor
7-
# TODO: undo
87
# TODO: Use "private alias_method" idiom after drop Ruby 2.5.
98
attr_reader :byte_pointer
109
attr_accessor :confirm_multiline_termination_proc
@@ -251,6 +250,8 @@ def reset_variables(prompt = '', encoding:)
251250
@resized = false
252251
@cache = {}
253252
@rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
253+
@past_lines = []
254+
@undoing = false
254255
reset_line
255256
end
256257

@@ -948,7 +949,8 @@ def dialog_proc_scope_completion_journey_data
948949
unless @waiting_proc
949950
byte_pointer_diff = @byte_pointer - old_byte_pointer
950951
@byte_pointer = old_byte_pointer
951-
send(@vi_waiting_operator, byte_pointer_diff)
952+
method_obj = method(@vi_waiting_operator)
953+
wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
952954
cleanup_waiting
953955
end
954956
else
@@ -1009,7 +1011,8 @@ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
10091011
if @vi_waiting_operator
10101012
byte_pointer_diff = @byte_pointer - old_byte_pointer
10111013
@byte_pointer = old_byte_pointer
1012-
send(@vi_waiting_operator, byte_pointer_diff)
1014+
method_obj = method(@vi_waiting_operator)
1015+
wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
10131016
cleanup_waiting
10141017
end
10151018
@kill_ring.process
@@ -1106,6 +1109,7 @@ def update(key)
11061109
end
11071110

11081111
def input_key(key)
1112+
save_old_buffer
11091113
@config.reset_oneshot_key_bindings
11101114
@dialogs.each do |dialog|
11111115
if key.char.instance_of?(Symbol) and key.char == dialog.name
@@ -1120,7 +1124,6 @@ def input_key(key)
11201124
finish
11211125
return
11221126
end
1123-
old_lines = @buffer_of_lines.dup
11241127
@first_char = false
11251128
@completion_occurs = false
11261129

@@ -1134,12 +1137,15 @@ def input_key(key)
11341137
@completion_journey_state = nil
11351138
end
11361139

1140+
push_past_lines unless @undoing
1141+
@undoing = false
1142+
11371143
if @in_pasting
11381144
clear_dialogs
11391145
return
11401146
end
11411147

1142-
modified = old_lines != @buffer_of_lines
1148+
modified = @old_buffer_of_lines != @buffer_of_lines
11431149
if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion
11441150
# Auto complete starts only when edited
11451151
process_insert(force: true)
@@ -1148,6 +1154,26 @@ def input_key(key)
11481154
modified
11491155
end
11501156

1157+
def save_old_buffer
1158+
@old_buffer_of_lines = @buffer_of_lines.dup
1159+
@old_byte_pointer = @byte_pointer.dup
1160+
@old_line_index = @line_index.dup
1161+
end
1162+
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])
1166+
end
1167+
trim_past_lines
1168+
end
1169+
1170+
MAX_PAST_LINES = 100
1171+
def trim_past_lines
1172+
if @past_lines.size > MAX_PAST_LINES
1173+
@past_lines.shift
1174+
end
1175+
end
1176+
11511177
def scroll_into_view
11521178
_wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
11531179
if wrapped_cursor_y < screen_scroll_top
@@ -1224,6 +1250,18 @@ def set_current_line(line, byte_pointer = nil)
12241250
process_auto_indent
12251251
end
12261252

1253+
def set_current_lines(lines, byte_pointer = nil, line_index = 0)
1254+
cursor = current_byte_pointer_cursor
1255+
@buffer_of_lines = lines
1256+
@line_index = line_index
1257+
if byte_pointer
1258+
@byte_pointer = byte_pointer
1259+
else
1260+
calculate_nearest_cursor(cursor)
1261+
end
1262+
process_auto_indent
1263+
end
1264+
12271265
def retrieve_completion_block(set_completion_quote_character = false)
12281266
if Reline.completer_word_break_characters.empty?
12291267
word_break_regexp = nil
@@ -1306,13 +1344,15 @@ def confirm_multiline_termination
13061344
end
13071345

13081346
def insert_pasted_text(text)
1347+
save_old_buffer
13091348
pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
13101349
post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
13111350
lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
13121351
lines << '' if lines.empty?
13131352
@buffer_of_lines[@line_index, 1] = lines
13141353
@line_index += lines.size - 1
13151354
@byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
1355+
push_past_lines
13161356
end
13171357

13181358
def insert_text(text)
@@ -2487,4 +2527,15 @@ def finish
24872527
private def vi_editing_mode(key)
24882528
@config.editing_mode = :vi_insert
24892529
end
2530+
2531+
private def undo(_key)
2532+
return if @past_lines.empty?
2533+
2534+
@undoing = true
2535+
2536+
target_lines, target_cursor_x, target_cursor_y = @past_lines.last
2537+
set_current_lines(target_lines, target_cursor_x, target_cursor_y)
2538+
2539+
@past_lines.pop
2540+
end
24902541
end

test/reline/test_key_actor_emacs.rb

+68
Original file line numberDiff line numberDiff line change
@@ -1437,4 +1437,72 @@ def test_vi_editing_mode
14371437
@line_editor.__send__(:vi_editing_mode, nil)
14381438
assert(@config.editing_mode_is?(:vi_insert))
14391439
end
1440+
1441+
def test_undo
1442+
input_keys("\C-_", false)
1443+
assert_line_around_cursor('', '')
1444+
input_keys("aあb\C-h\C-h\C-h", false)
1445+
assert_line_around_cursor('', '')
1446+
input_keys("\C-_", false)
1447+
assert_line_around_cursor('a', '')
1448+
input_keys("\C-_", false)
1449+
assert_line_around_cursor('aあ', '')
1450+
input_keys("\C-_", false)
1451+
assert_line_around_cursor('aあb', '')
1452+
input_keys("\C-_", false)
1453+
assert_line_around_cursor('aあ', '')
1454+
input_keys("\C-_", false)
1455+
assert_line_around_cursor('a', '')
1456+
input_keys("\C-_", false)
1457+
assert_line_around_cursor('', '')
1458+
end
1459+
1460+
def test_undo_with_cursor_position
1461+
input_keys("abc\C-b\C-h", false)
1462+
assert_line_around_cursor('a', 'c')
1463+
input_keys("\C-_", false)
1464+
assert_line_around_cursor('ab', 'c')
1465+
input_keys("あいう\C-b\C-h", false)
1466+
assert_line_around_cursor('abあ', 'うc')
1467+
input_keys("\C-_", false)
1468+
assert_line_around_cursor('abあい', 'うc')
1469+
end
1470+
1471+
def test_undo_with_multiline
1472+
@line_editor.multiline_on
1473+
@line_editor.confirm_multiline_termination_proc = proc {}
1474+
input_keys("1\n2\n3", false)
1475+
assert_whole_lines(["1", "2", "3"])
1476+
assert_line_index(2)
1477+
assert_line_around_cursor('3', '')
1478+
input_keys("\C-p\C-h\C-h", false)
1479+
assert_whole_lines(["1", "3"])
1480+
assert_line_index(0)
1481+
assert_line_around_cursor('1', '')
1482+
input_keys("\C-_", false)
1483+
assert_whole_lines(["1", "", "3"])
1484+
assert_line_index(1)
1485+
assert_line_around_cursor('', '')
1486+
input_keys("\C-_", false)
1487+
assert_whole_lines(["1", "2", "3"])
1488+
assert_line_index(1)
1489+
assert_line_around_cursor('2', '')
1490+
input_keys("\C-_", false)
1491+
assert_whole_lines(["1", "2", ""])
1492+
assert_line_index(2)
1493+
assert_line_around_cursor('', '')
1494+
input_keys("\C-_", false)
1495+
assert_whole_lines(["1", "2"])
1496+
assert_line_index(1)
1497+
assert_line_around_cursor('2', '')
1498+
end
1499+
1500+
def test_undo_with_many_times
1501+
str = "a" + "b" * 100
1502+
input_keys(str, false)
1503+
100.times { input_keys("\C-_", false) }
1504+
assert_line_around_cursor('a', '')
1505+
input_keys("\C-_", false)
1506+
assert_line_around_cursor('a', '')
1507+
end
14401508
end

test/reline/yamatanooroti/test_rendering.rb

+13
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,19 @@ def test_bracketed_paste
556556
EOC
557557
end
558558

559+
def test_bracketed_paste_with_undo
560+
omit if Reline.core.io_gate.win?
561+
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
562+
write("abc")
563+
write("\e[200~def hoge\r\t3\rend\e[201~")
564+
write("\C-_")
565+
close
566+
assert_screen(<<~EOC)
567+
Multiline REPL.
568+
prompt> abc
569+
EOC
570+
end
571+
559572
def test_backspace_until_returns_to_initial
560573
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
561574
write("ABC")

0 commit comments

Comments
 (0)