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

In ed_search_[prev|next]_history, make the cursor come to the end of the line when there is no search substr #714

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ module CompletionState
RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true)

CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer)
ActionState = Struct.new(:state_type, :state_value)
NullActionState = ActionState.new(nil, nil)
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 have used a Struct to make the contents of action_state more understandable, but using a simple Array would also be good.

Copy link
Member

Choose a reason for hiding this comment

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

In ed_search_prev_history and ed_search_next_history, @prev_action_state == ActionState.new(:search_history, :empty) is working great 👍

In general, I think this kind of ideom will appear frequently.

foobar = @prev_action_state.value if @prev_action_state&.type == :foobar

To make it easy to use, can you add a helper method like this?

# get
prev = prev_action_state(:search_history)
# set
set_next_action_state(:search_history, :empty)

Using it, we can just use a simple array internally. It will reduce total struct/class count (complexity).
And we don't need to think of NullActionState wrongly modified.

state = @prev_action_state
state.value = next_value # bug. this might modify NullActionState
@next_action_state = state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks good! I tried to represent the state in a 2-element array!
19dae5b


class MenuInfo
attr_reader :list
Expand Down Expand Up @@ -1133,6 +1135,9 @@ def input_key(key)
else
normal_char(key)
end

@prev_action_state, @next_action_state = @next_action_state, NullActionState
Copy link
Member

Choose a reason for hiding this comment

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

Can you add an initialization of these instance variables in def reset_variables?
This will properly re-initialize just ater Reline.readline is canceled by Ctrl-C.
Initial state of @prev_action_state will be simple. (Currently, initial value is nil and will be updated to NullActionState)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the advice. I have also added initialization of variables at 19dae5b


unless @completion_occurs
@completion_state = CompletionState::NORMAL
@completion_journey_state = nil
Expand Down Expand Up @@ -1763,29 +1768,31 @@ def finish
end

private def ed_search_prev_history(key, arg: 1)
substr = current_line.byteslice(0, @byte_pointer)
substr = @prev_action_state == ActionState.new(:search_history, :empty) ? '' : current_line.byteslice(0, @byte_pointer)
return if @history_pointer == 0
return if @history_pointer.nil? && substr.empty? && !current_line.empty?

history_range = 0...(@history_pointer || Reline::HISTORY.size)
h_pointer, line_index = search_history(substr, history_range.reverse_each)
return unless h_pointer
move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
arg -= 1
@next_action_state = substr.empty? ? ActionState.new(:search_history, :empty) : NullActionState
ed_search_prev_history(key, arg: arg) if arg > 0
end
alias_method :history_search_backward, :ed_search_prev_history

private def ed_search_next_history(key, arg: 1)
substr = current_line.byteslice(0, @byte_pointer)
substr = @prev_action_state == ActionState.new(:search_history, :empty) ? '' : current_line.byteslice(0, @byte_pointer)
return if @history_pointer.nil?

history_range = @history_pointer + 1...Reline::HISTORY.size
h_pointer, line_index = search_history(substr, history_range)
return if h_pointer.nil? and not substr.empty?

move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
arg -= 1
@next_action_state = substr.empty? ? ActionState.new(:search_history, :empty) : NullActionState
ed_search_next_history(key, arg: arg) if arg > 0
end
alias_method :history_search_forward, :ed_search_next_history
Expand Down
55 changes: 35 additions & 20 deletions test/reline/test_key_actor_emacs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1242,14 +1242,22 @@ def test_ed_search_prev_history_with_empty
'12345' # new
])
# The ed_search_prev_history doesn't have default binding
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12345')
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12aaa')
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12356')
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12356')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12345', '')
Copy link
Member

@tompng tompng Jun 2, 2024

Choose a reason for hiding this comment

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

This part should be ('12aaa', '')
#618 might be harder than I first thought.

I checked GNU Readline.

ruby -rreadline -rreline -e "raise if Readline==Reline; loop{p Readline.readline 'prompt> ', true}"
prompt> █
ed_search_prev_history, search with empty string
prompt> 12345█
(A) ed_search_prev_history, search with empty string
prompt> 12aaa█
prompt> █
ed_search_prev_history, search with empty string
prompt> 12345█
Move cursor left and right. Continuous search_xxx_history is reset.
prompt> 12345█
(B) ed_search_prev_history, search with "12345", search fails
prompt> 12345█

There is a difference between (A) and (B). Same input, same cursor position, but there is a hidden state. We need to introduce a new state (instance variable) 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.

Adding new instance variable for many def ed_[key_action_name] will make maintainability poor, so I think we should introduce a general-purpose feature for passing state to next action.

One idea is:

def input_key
  ...
  # process_key() or normal_char() is called here
  ...
  @prev_action_state, @next_action_state = @next_action_state, nil
end

Key action method can read @prev_action_state.
If the action want to pass a state to next action, set @next_action_state to something like [state_type, state_value].

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 clear comment. To replicate the behavior of Readline, it is best to include information about the before and after states of the current operation. I will give it a try

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 7ddd20d

input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12356', '')
input_key_by_symbol(:ed_search_next_history)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_prev_char)
input_key_by_symbol(:ed_next_char)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12aaa', '')
3.times { input_key_by_symbol(:ed_prev_char) }
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12', '356')
end

def test_ed_search_prev_history_without_match
Expand Down Expand Up @@ -1291,18 +1299,25 @@ def test_ed_search_next_history_with_empty
'12345' # new
])
# The ed_search_prev_history and ed_search_next_history doesn't have default binding
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12345')
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12aaa')
@line_editor.__send__(:ed_search_prev_history, "\C-p".ord)
assert_line_around_cursor('', '12356')
@line_editor.__send__(:ed_search_next_history, "\C-n".ord)
assert_line_around_cursor('', '12aaa')
@line_editor.__send__(:ed_search_next_history, "\C-n".ord)
assert_line_around_cursor('', '12345')
@line_editor.__send__(:ed_search_next_history, "\C-n".ord)
assert_line_around_cursor('', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12345', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12356', '')
input_key_by_symbol(:ed_search_next_history)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_search_next_history)
assert_line_around_cursor('12345', '')
input_key_by_symbol(:ed_search_prev_history)
assert_line_around_cursor('12aaa', '')
input_key_by_symbol(:ed_prev_char)
input_key_by_symbol(:ed_next_char)
input_key_by_symbol(:ed_search_next_history)
assert_line_around_cursor('12aaa', '')
3.times { input_key_by_symbol(:ed_prev_char) }
input_key_by_symbol(:ed_search_next_history)
assert_line_around_cursor('12', '345')
end

def test_incremental_search_history_cancel_by_symbol_key
Expand Down
Loading