Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the redo command #707

Merged
merged 11 commits into from
May 27, 2024
Merged

Implement the redo command #707

merged 11 commits into from
May 27, 2024

Conversation

verdy89
Copy link
Contributor

@verdy89 verdy89 commented May 18, 2024

Overview

Implemented the redo method to enable reverting undone actions on a per-action basis.

Implementation Approach

By defining @position, I was able to implement the redo command using only @past_lines, which was originally defined for the undo command. By saving the current line state in @past_lines, the entire mechanism was simplified. Specifically, it works as follows:

  • Normal input: Delete all elements after the current position in @past_lines, push the input into @past_lines, and increase @position by 1.
  • Cursor movement: Update only the cursor position in @past_lines.
  • Undo: Decrease @position by 1 and return @past_lines[position].
  • Redo: Increase @position by 1 and return @past_lines[position].

@verdy89 verdy89 marked this pull request as ready for review May 20, 2024 04:24
@@ -15,7 +15,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 6 ^F
:ed_next_char,
# 7 ^G
:ed_unassigned,
:redo,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about setting redo to M-C-_? Maybe it's used in Emacs.
C-g is assigned abort in GNU Readline, so I believe it is better to avoid using this.

https://www.gnu.org/savannah-checkouts/gnu/emacs/news/NEWS.User.20.html

Several commands in the group buffer can be undone with M-C-_

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the advice. As you suggested, I have changed the key assignment.

Changed redo key assignment from \C-g to \M-\C-_

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about managing the history using only @past_lines with an index in an instance variable?
This way, it would be more efficient as it avoids moving entries between two arrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we aim to use only @past_lines, it may be necessary to provide instance variables such as @position to implement the redo. It seems that this would result in a complex mechanism like below.

def normal_input
  if @past_lines.length > @position
    @past_lines = @past_lines.slice(0, @position + 1)
  else
    @past_lines.push(current_line)
  end
  @position += 1
end

def undo
  if @past_lines.length == @position
    @past_lines.push(current_line)
  end

  @position -= 1
  current_line = @past_lines[@position]
end

def redo
  if @past_lines.length - 1 > @position
    @position += 1
    current_line = @past_lines[@position]
  end
end

It may be true that moving elements between two arrays is not efficient, but even when using only @past_lines, the amount of data that needs to be stored remains the same. Additionally, having a more complex mechanism would make maintenance more challenging.

Is there a better way to achieve this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for writing the sample code! As you mentioned, we need to use instance variables like @position. Similar operations are performed in the methods that handle history, as shown here: https://github.com/ruby/reline/blob/master/lib/reline/line_editor.rb#L1601-L1843.
While this approach is more complex than using push/pop with two arrays, I don't think it reduces maintainability.

Personally, I feel there are too many instance variables in line_editor.rb, so I would like to reduce the number of instance variables with significant responsibilities. I find that using integers has a narrower scope of responsibility than using arrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I understand. Thank you for your comments.
With additional advice from @tompng , I was able to implement redo without using @next_lines!

https://github.com/ruby/reline/pull/707/files/9e0d6ad14e18173930419f1e617fb44608984e14..99cf6c1b1a8485ad78e1b6720eb935884a80ab7b

@@ -1498,11 +1498,115 @@ def test_undo_with_multiline
end

def test_undo_with_many_times
str = "a" + "b" * 100
str = "a" + "b" * 99
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I changed the implementation to push up to the current state into @past_lines, the number of actions stored in @past_lines is now one less than MAX_PAST_LINES. However, I believe that the value of MAX_PAST_LINES being set to 100 was chosen simply because 100 is a round number, without any other specific reason. Therefore, instead of changing the value of MAX_PAST_LINES, I addressed this by modifying the tests.

@@ -1137,7 +1138,10 @@ def input_key(key)
@completion_journey_state = nil
end

push_past_lines unless @undoing
unless @undoing
@past_lines = @past_lines[0..@position]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this part needs to be implemented inside push_past_lines because:

  • push_past_lines is also called from insert_pasted_text. This part is also needed in there.
  • This will blow redo history even when cursor is just moved. This part should be done only when @old_buffer_of_lines != @buffer_of_lines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment. I've fixed it.

Fixed deletion of the redo history in regular input

Comment on lines 253 to 254
@past_lines = [[[""], 0, 0]]
@position = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instance variable name position feels too broad. Since the line_editor.rb is a large file, we want to narrow down what position refers to. For example, if it’s related to input_lines, we could use input_lines_position.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with input_lines_position as well.

Rename @position to @input_lines_position

@@ -250,7 +250,8 @@ def reset_variables(prompt = '', encoding:)
@resized = false
@cache = {}
@rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
@past_lines = []
@past_lines = [[[""], 0, 0]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we can now move through the past and future with undo and redo, the variable name past_lines doesn't seem very appropriate. As a suggestion, how about input_lines? If you have any other ideas, please let me know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I couldn't come up with a better name, I went with input_lines. Thank you.

Renamed variables: past_lines -> input_lines

@@ -1161,8 +1162,12 @@ def save_old_buffer
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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like @old_byte_pointer and @old_line_index are not used anymore. We can remove initialization code from def save_old_buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed initialization code. Thank you.

Deleted unused variables: @old_byte_pointer and @old_line_index

Copy link
Member

@ima1zumi ima1zumi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work 👍 Thanks for your contribution!

Copy link
Member

@tompng tompng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@tompng tompng merged commit 0b2d9fa into ruby:master May 27, 2024
40 checks passed
matzbot pushed a commit to ruby/ruby that referenced this pull request May 27, 2024
(ruby/reline#707)

* Implement the redo command

* Commented out a test that does not pass

* Changed key assignment for redo from "\C-[" to "\C-g"

* Changed redo key assignment from `\C-g` to `\M-\C-_`

* Revert the first implemantation

* Implemented redo by sharing `@past_lines` between undo and redo

* Fixed the index of past_lines that is updated when the cursor is moved

* Fixed deletion of the redo history in regular input

* Renamed variables: past_lines -> input_lines

* Rename @position to @input_lines_position

* Deleted unused variables: `@old_byte_pointer` and `@old_line_index`

ruby/reline@0b2d9fab5f
@ima1zumi ima1zumi added the enhancement New feature or request label May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

Successfully merging this pull request may close these issues.

None yet

3 participants