Skip to content

Commit

Permalink
Implement reverse-i-search on ctrl+r, and add a disable_search? h…
Browse files Browse the repository at this point in the history
…ook.
  • Loading branch information
I3oris committed Aug 31, 2024
1 parent 5da267e commit 525083d
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ It includes the following features:
* Hook for Auto formatting
* Hook for Auto indentation
* Hook for Auto completion (Experimental)
* History Reverse i-search
* Work on Windows 10

It doesn't support yet:
* History reverse i-search
* Customizable hotkeys
* Unicode characters

Expand Down
3 changes: 3 additions & 0 deletions src/char_reader.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Reply
CTRL_K
CTRL_N
CTRL_P
CTRL_R
CTRL_U
CTRL_X
CTRL_UP
Expand Down Expand Up @@ -141,6 +142,8 @@ module Reply
Sequence::CTRL_N
when ctrl('p')
Sequence::CTRL_P
when ctrl('r')
Sequence::CTRL_R
when ctrl('u')
Sequence::CTRL_U
when ctrl('x')
Expand Down
33 changes: 31 additions & 2 deletions src/expression_editor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ module Reply

@scroll_offset = 0
@header_height = 0
@footer_height = 0

@header : IO, Int32 -> Int32 = ->(io : IO, previous_height : Int32) { 0 }
@footer : IO, Int32 -> Int32 = ->(io : IO, previous_height : Int32) { 0 }
@highlight = ->(code : String) { code }

# The list of characters delimiting words.
Expand All @@ -101,11 +103,19 @@ module Reply
# Sets a `Proc` allowing to display a header above the prompt. (used by auto-completion)
#
# *io*: The IO in which the header should be displayed.
# *previous_hight*: Previous header height, useful to keep a header size constant.
# *previous_height*: Previous header height, useful to keep a header size constant.
# Should returns the exact *height* printed in the io.
def set_header(&@header : IO, Int32 -> Int32)
end

# Sets a `Proc` allowing to display a footer under the prompt. (used by search)
#
# *io*: The IO in which the footer should be displayed.
# *previous_height*: Previous footer height.
# Should returns the exact *height* printed in the io.
def set_footer(&@footer : IO, Int32 -> Int32)
end

# Sets the `Proc` to highlight the expression.
def set_highlight(&@highlight : String -> String)
end
Expand Down Expand Up @@ -383,7 +393,7 @@ module Reply
#
# The expression scrolls if it's higher than epression_max_height.
private def epression_max_height
self.height - @header_height
self.height - @header_height - @footer_height
end

def move_cursor_left(allow_scrolling = true)
Expand Down Expand Up @@ -724,6 +734,13 @@ module Reply
end
end

# Calls the footer proc and saves the *footer_height*
private def update_footer : String
String.build do |io|
@footer_height = @footer.call(io, @footer_height)
end
end

def replace(lines : Array(String))
update { @lines = lines }
end
Expand Down Expand Up @@ -873,6 +890,7 @@ module Reply
private def print_expression_and_header(height_to_clear, force_full_view = false)
height_to_clear += @header_height
header = update_header()
footer = update_footer()

if force_full_view
start, end_ = 0, Int32::MAX
Expand Down Expand Up @@ -945,6 +963,17 @@ module Reply
@output.print header
@output.print display

if @footer_height != 0
# Display footer, then rewind cursor at the top left of the footer
@output.puts
@output.print footer
@output.print Term::Cursor.column(1)
move_real_cursor(x: @prompt_size, y: 1 - @footer_height)

cursor_move_y += 1
cursor_move_x = 0
end

# Retrieve the real cursor at its corresponding cursor position (`@x`, `@y`)
x_save, y_save = @x, @y
@y = cursor_move_y
Expand Down
34 changes: 32 additions & 2 deletions src/history.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ module Reply
class History
getter history = Deque(Array(String)).new
getter max_size = 10_000
@index = 0
getter index = 0

# Hold the history lines being edited, always contains one element more than @history
# because it can also contain the current line (not yet in history)
@edited_history = [nil] of Array(String)?

def size
@history.size
end

def <<(lines)
lines = lines.dup # make history elements independent

Expand Down Expand Up @@ -45,8 +49,34 @@ module Reply
@edited_history[@index] = current_edited_lines

@index += 1
(@edited_history[@index]? || @history[@index]).dup
(@edited_history[@index]? || @history[@index]? || [""]).dup
end
end

def go_to(index)
if 0 <= index < @history.size
@index = index

@history[@index].dup
end
end

def search_up(query, from_index = @index - 1)
return nil, 0, 0 if query.empty?
return nil, 0, 0 unless 0 <= from_index < @history.size

# Search the history starting by `from_index` until first entry,
# then cycle the search by searching from last entry to `from_index`
from_index.downto(0).chain(
(@history.size - 1).downto(from_index + 1)
).each do |i|
@history[i].each_with_index do |line, y|
x = line.index query
return i, x, y if x
end
end

return nil, 0, 0
end

def max_size=(max_size)
Expand Down
55 changes: 54 additions & 1 deletion src/reader.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "./history"
require "./expression_editor"
require "./char_reader"
require "./auto_completion"
require "./search"

module Reply
# Reader for your REPL.
Expand Down Expand Up @@ -45,12 +46,14 @@ module Reply
# ^ ^
# | |
# History AutoCompletion
# +Search
# ```

getter history = History.new
getter editor : ExpressionEditor
@auto_completion : AutoCompletion
@char_reader = CharReader.new
@search = Search.new
getter line_number = 1

delegate :color?, :color=, :lines, :output, :output=, to: @editor
Expand All @@ -72,6 +75,10 @@ module Reply
@auto_completion.display_entries(io, color?, max_height: {10, Term::Size.height - 1}.min, min_height: previous_height)
end

@editor.set_footer do |io, _previous_height|
@search.footer(io, color?)
end

@editor.set_highlight(&->highlight(String))

if file = self.history_file
Expand Down Expand Up @@ -118,7 +125,7 @@ module Reply
0
end

# Override to select with expression is saved in history.
# Override to select which expression is saved in history.
#
# default: `!expression.blank?`
def save_in_history?(expression : String)
Expand All @@ -132,6 +139,13 @@ module Reply
nil
end

# Override with `true` to disable the reverse i-search (ctrl-r).
#
# default: `true` (disabled) if `history_file` not set.
def disable_search?
history_file.nil?
end

# Override to integrate auto-completion.
#
# *current_word* is picked following `word_delimiters`.
Expand Down Expand Up @@ -232,6 +246,7 @@ module Reply
in .ctrl_delete? then @editor.update { delete_word }
in .alt_d? then @editor.update { delete_word }
in .ctrl_c? then on_ctrl_c
in .ctrl_r? then on_ctrl_r
in .ctrl_d?
if @editor.empty?
output.puts
Expand All @@ -244,6 +259,12 @@ module Reply
return nil
end

if (read.is_a?(CharReader::Sequence) && (read.ctrl_r? || read.backspace?)) || read.is_a?(Char) || read.is_a?(String)
else
@search.close
@editor.update
end

if read.is_a?(CharReader::Sequence) && (read.tab? || read.enter? || read.alt_enter? || read.shift_tab? || read.escape? || read.backspace? || read.ctrl_c?)
else
if @auto_completion.open?
Expand Down Expand Up @@ -278,6 +299,8 @@ module Reply
end

private def on_char(char)
return search_and_replace(@search.query + char) if @search.open?

@editor.update do
@editor << char
line = @editor.current_line.rstrip(' ')
Expand All @@ -294,13 +317,21 @@ module Reply
end

private def on_string(string)
return search_and_replace(@search.query + string) if @search.open?

@editor.update do
@editor << string
end
end

private def on_enter(alt_enter = false, ctrl_enter = false, &)
@auto_completion.close
if @search.open?
@search.close
@editor.update
return
end

if alt_enter || ctrl_enter || (@editor.cursor_on_last_line? && continue?(@editor.expression))
@editor.update do
insert_new_line(indent: self.indentation_level(@editor.expression_before_cursor))
Expand Down Expand Up @@ -338,6 +369,8 @@ module Reply
end

private def on_back
return search_and_replace(@search.query.rchop) if @search.open?

auto_complete_remove_char if @auto_completion.open?
@editor.update { back }
end
Expand Down Expand Up @@ -365,12 +398,21 @@ module Reply

private def on_ctrl_c
@auto_completion.close
@search.close
@editor.end_editing
output.puts "^C"
@history.set_to_last
@editor.prompt_next
end

private def on_ctrl_r
return if disable_search?

@auto_completion.close
@search.open
search_and_replace(reuse_index?: true)
end

private def on_tab(shift_tab = false)
if @auto_completion.open?
if shift_tab
Expand All @@ -390,6 +432,7 @@ module Reply

private def on_escape
@auto_completion.close
@search.close
@editor.update
end

Expand Down Expand Up @@ -446,6 +489,16 @@ module Reply
end
end

private def search_and_replace(query = nil, reuse_index? = false)
@search.query = query if query

from_index = reuse_index? ? @history.index - 1 : @history.size - 1
result, x, y = @search.search(@history, from_index)
@editor.replace(result || [""])

@editor.move_cursor_to(x + @search.query.size, y) if result
end

private def submit_expr(*, history = true)
formated = format(@editor.expression).try &.split('\n')
@editor.end_editing(replacement: formated)
Expand Down
39 changes: 39 additions & 0 deletions src/search.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module Reply
class Search
getter? open = false
property query = ""
getter? failed = false

def footer(io : IO, color? : Bool)
if open?
io << "search: #{@query.colorize.toggle(failed? && color?).bold.red}_"
1
else
0
end
end

def open
@open = true
@failed = false
end

def close
@open = false
@query = ""
end

def search(history, from_index = history.index)
index, x, y = history.search_up(@query, from_index: from_index)
if index
@failed = false
result = history.go_to index
return result, x, y
end

@failed = true
history.set_to_last
return nil, 0, 0
end
end
end

0 comments on commit 525083d

Please sign in to comment.