Skip to content

Commit

Permalink
Implement the undo command (#701)
Browse files Browse the repository at this point in the history
* 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`
  • Loading branch information
ima1zumi authored May 14, 2024
1 parent 21891c4 commit 4ab72f9
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 6 deletions.
2 changes: 1 addition & 1 deletion lib/reline/key_actor/emacs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 30 ^^
:ed_unassigned,
# 31 ^_
:ed_unassigned,
:undo,
# 32 SPACE
:ed_insert,
# 33 !
Expand Down
61 changes: 56 additions & 5 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
require 'tempfile'

class Reline::LineEditor
# TODO: undo
# TODO: Use "private alias_method" idiom after drop Ruby 2.5.
attr_reader :byte_pointer
attr_accessor :confirm_multiline_termination_proc
Expand Down Expand Up @@ -251,6 +250,8 @@ def reset_variables(prompt = '', encoding:)
@resized = false
@cache = {}
@rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
@past_lines = []
@undoing = false
reset_line
end

Expand Down Expand Up @@ -948,7 +949,8 @@ def dialog_proc_scope_completion_journey_data
unless @waiting_proc
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
send(@vi_waiting_operator, byte_pointer_diff)
method_obj = method(@vi_waiting_operator)
wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
cleanup_waiting
end
else
Expand Down Expand Up @@ -1009,7 +1011,8 @@ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
if @vi_waiting_operator
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
send(@vi_waiting_operator, byte_pointer_diff)
method_obj = method(@vi_waiting_operator)
wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
cleanup_waiting
end
@kill_ring.process
Expand Down Expand Up @@ -1106,6 +1109,7 @@ def update(key)
end

def input_key(key)
save_old_buffer
@config.reset_oneshot_key_bindings
@dialogs.each do |dialog|
if key.char.instance_of?(Symbol) and key.char == dialog.name
Expand All @@ -1120,7 +1124,6 @@ def input_key(key)
finish
return
end
old_lines = @buffer_of_lines.dup
@first_char = false
@completion_occurs = false

Expand All @@ -1134,12 +1137,15 @@ def input_key(key)
@completion_journey_state = nil
end

push_past_lines unless @undoing
@undoing = false

if @in_pasting
clear_dialogs
return
end

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

def save_old_buffer
@old_buffer_of_lines = @buffer_of_lines.dup
@old_byte_pointer = @byte_pointer.dup
@old_line_index = @line_index.dup
end

def push_past_lines
if @old_buffer_of_lines != @buffer_of_lines
@past_lines.push([@old_buffer_of_lines, @old_byte_pointer, @old_line_index])
end
trim_past_lines
end

MAX_PAST_LINES = 100
def trim_past_lines
if @past_lines.size > MAX_PAST_LINES
@past_lines.shift
end
end

def scroll_into_view
_wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
if wrapped_cursor_y < screen_scroll_top
Expand Down Expand Up @@ -1224,6 +1250,18 @@ def set_current_line(line, byte_pointer = nil)
process_auto_indent
end

def set_current_lines(lines, byte_pointer = nil, line_index = 0)
cursor = current_byte_pointer_cursor
@buffer_of_lines = lines
@line_index = line_index
if byte_pointer
@byte_pointer = byte_pointer
else
calculate_nearest_cursor(cursor)
end
process_auto_indent
end

def retrieve_completion_block(set_completion_quote_character = false)
if Reline.completer_word_break_characters.empty?
word_break_regexp = nil
Expand Down Expand Up @@ -1306,13 +1344,15 @@ def confirm_multiline_termination
end

def insert_pasted_text(text)
save_old_buffer
pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
lines << '' if lines.empty?
@buffer_of_lines[@line_index, 1] = lines
@line_index += lines.size - 1
@byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
push_past_lines
end

def insert_text(text)
Expand Down Expand Up @@ -2487,4 +2527,15 @@ def finish
private def vi_editing_mode(key)
@config.editing_mode = :vi_insert
end

private def undo(_key)
return if @past_lines.empty?

@undoing = true

target_lines, target_cursor_x, target_cursor_y = @past_lines.last
set_current_lines(target_lines, target_cursor_x, target_cursor_y)

@past_lines.pop
end
end
68 changes: 68 additions & 0 deletions test/reline/test_key_actor_emacs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1437,4 +1437,72 @@ def test_vi_editing_mode
@line_editor.__send__(:vi_editing_mode, nil)
assert(@config.editing_mode_is?(:vi_insert))
end

def test_undo
input_keys("\C-_", false)
assert_line_around_cursor('', '')
input_keys("aあb\C-h\C-h\C-h", false)
assert_line_around_cursor('', '')
input_keys("\C-_", false)
assert_line_around_cursor('a', '')
input_keys("\C-_", false)
assert_line_around_cursor('aあ', '')
input_keys("\C-_", false)
assert_line_around_cursor('aあb', '')
input_keys("\C-_", false)
assert_line_around_cursor('aあ', '')
input_keys("\C-_", false)
assert_line_around_cursor('a', '')
input_keys("\C-_", false)
assert_line_around_cursor('', '')
end

def test_undo_with_cursor_position
input_keys("abc\C-b\C-h", false)
assert_line_around_cursor('a', 'c')
input_keys("\C-_", false)
assert_line_around_cursor('ab', 'c')
input_keys("あいう\C-b\C-h", false)
assert_line_around_cursor('abあ', 'うc')
input_keys("\C-_", false)
assert_line_around_cursor('abあい', 'うc')
end

def test_undo_with_multiline
@line_editor.multiline_on
@line_editor.confirm_multiline_termination_proc = proc {}
input_keys("1\n2\n3", false)
assert_whole_lines(["1", "2", "3"])
assert_line_index(2)
assert_line_around_cursor('3', '')
input_keys("\C-p\C-h\C-h", false)
assert_whole_lines(["1", "3"])
assert_line_index(0)
assert_line_around_cursor('1', '')
input_keys("\C-_", false)
assert_whole_lines(["1", "", "3"])
assert_line_index(1)
assert_line_around_cursor('', '')
input_keys("\C-_", false)
assert_whole_lines(["1", "2", "3"])
assert_line_index(1)
assert_line_around_cursor('2', '')
input_keys("\C-_", false)
assert_whole_lines(["1", "2", ""])
assert_line_index(2)
assert_line_around_cursor('', '')
input_keys("\C-_", false)
assert_whole_lines(["1", "2"])
assert_line_index(1)
assert_line_around_cursor('2', '')
end

def test_undo_with_many_times
str = "a" + "b" * 100
input_keys(str, false)
100.times { input_keys("\C-_", false) }
assert_line_around_cursor('a', '')
input_keys("\C-_", false)
assert_line_around_cursor('a', '')
end
end
13 changes: 13 additions & 0 deletions test/reline/yamatanooroti/test_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,19 @@ def test_bracketed_paste
EOC
end

def test_bracketed_paste_with_undo
omit if Reline.core.io_gate.win?
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
write("abc")
write("\e[200~def hoge\r\t3\rend\e[201~")
write("\C-_")
close
assert_screen(<<~EOC)
Multiline REPL.
prompt> abc
EOC
end

def test_backspace_until_returns_to_initial
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
write("ABC")
Expand Down

0 comments on commit 4ab72f9

Please sign in to comment.