diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 8efd54896c798..1e1a8a9fcde47 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -68,6 +68,7 @@ mutable struct PromptState <: ModeState p::Prompt input_buffer::IOBuffer undo_buffers::Vector{IOBuffer} + undo_idx::Int ias::InputAreaState # indentation of lines which do not include the prompt # if negative, the width of the prompt is used @@ -1668,7 +1669,8 @@ AnyDict( # Meta Enter "\e\r" => (s,o...)->edit_insert_newline(s), "\e\n" => "\e\r", - "^_" => (s,o...)->(pop_undo(s) ? refresh_line(s) : beep(terminal(s))), + "^_" => (s,o...)->edit_undo!(s), + "\e_" => (s,o...)->edit_redo!(s), # Simply insert it into the buffer by default "*" => (s,data,c)->(edit_insert(s, c)), "^U" => (s,o...)->edit_clear(s), @@ -1856,7 +1858,7 @@ end run_interface(::Prompt) = nothing init_state(terminal, prompt::Prompt) = - PromptState(terminal, prompt, IOBuffer(), IOBuffer[], InputAreaState(1, 1), + PromptState(terminal, prompt, IOBuffer(), IOBuffer[], 1, InputAreaState(1, 1), #=indent(spaces)=# -1) function init_state(terminal, m::ModalInterface) @@ -1895,20 +1897,59 @@ position(s::Union{MIState,ModeState}) = position(buffer(s)) function empty_undo(s::PromptState) empty!(s.undo_buffers) + s.undo_idx = 1 end + empty_undo(s) = nothing -function push_undo(s::PromptState) - push!(s.undo_buffers, copy(s.input_buffer)) +function push_undo(s::PromptState, advance=true) + resize!(s.undo_buffers, s.undo_idx) + s.undo_buffers[end] = copy(s.input_buffer) + advance && (s.undo_idx += 1) end + push_undo(s) = nothing +# must be called after a push_undo function pop_undo(s::PromptState) - length(s.undo_buffers) > 0 || return false - s.input_buffer = pop!(s.undo_buffers) + pop!(s.undo_buffers) + s.undo_idx -= 1 +end + +function edit_undo!(s::MIState) + s.last_action ∉ (:edit_redo!, :edit_undo!) && push_undo(s, false) + if edit_undo!(state(s)) + :edit_undo! + else + beep(terminal(s)) + :ignore + end +end + +function edit_undo!(s::PromptState) + s.undo_idx > 1 || return false + s.input_buffer = s.undo_buffers[s.undo_idx -=1] + refresh_line(s) + true +end +edit_undo!(s) = nothing + +function edit_redo!(s::MIState) + if s.last_action ∈ (:edit_redo!, :edit_undo!) && edit_redo!(state(s)) + :edit_redo! + else + beep(terminal(s)) + :ignore + end +end + +function edit_redo!(s::PromptState) + s.undo_idx < length(s.undo_buffers) || return false + s.input_buffer = s.undo_buffers[s.undo_idx += 1] + refresh_line(s) true end -pop_undo(s) = nothing +edit_redo!(s) = nothing keymap(s::PromptState, prompt::Prompt) = prompt.keymap_dict keymap_data(s::PromptState, prompt::Prompt) = prompt.keymap_func_data diff --git a/test/lineedit.jl b/test/lineedit.jl index 194a6b4d676c8..8dc8d5aad2a53 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -22,8 +22,11 @@ charpos(buf, pos=position(buf)) = Base.unsafe_ind2chr(content(buf), pos+1)-1 function transform!(f, s, i = -1) # i is char-based (not bytes) buffer position buf = buffer(s) i >= 0 && charseek(buf, i) - f(s) - content(buf), charpos(buf), charpos(buf, getmark(buf)) + action = f(s) + if s isa LineEdit.MIState && action isa Symbol + s.last_action = action # simulate what happens in LineEdit.prompt! + end + content(s), charpos(buf), charpos(buf, getmark(buf)) end @@ -604,10 +607,11 @@ end @test s.kill_ring[end] == "a ≡ not" charseek(buf, 0) @test transform!(LineEdit.edit_yank, s) == ("a ≡ notçhing", 7, 0) + s.last_action = :unknown # next action will fail, as yank-pop doesn't know a yank was just issued @test transform!(LineEdit.edit_yank_pop, s) == ("a ≡ notçhing", 7, 0) s.last_action = :edit_yank - # not this should work: + # now this should work: @test transform!(LineEdit.edit_yank_pop, s) == ("ça ≡ nothingçhing", 12, 0) @test s.kill_idx == 1 LineEdit.edit_kill_line(s) @@ -617,88 +621,78 @@ end @testset "undo" begin s = new_state() + edit!(f) = transform!(f, s)[1] + edit_undo! = LineEdit.edit_undo! + edit_redo! = LineEdit.edit_redo! edit_insert(s, "one two three") - LineEdit.edit_delete_prev_word(s) - @test content(s) == "one two " - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_delete_prev_word) == "one two " + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_redo!) == "one two " + @test edit!(edit_undo!) == "one two three" edit_insert(s, " four") - edit_insert(s, " five") - @test content(s) == "one two three four five" - LineEdit.pop_undo(s) - @test content(s) == "one two three four" - LineEdit.pop_undo(s) - @test content(s) == "one two three" - - LineEdit.edit_clear(s) - @test content(s) == "" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(s->edit_insert(s, " five")) == "one two three four five" + @test edit!(edit_undo!) == "one two three four" + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_redo!) == "one two three four" + @test edit!(edit_redo!) == "one two three four five" + @test edit!(edit_undo!) == "one two three four" + @test edit!(edit_undo!) == "one two three" + + @test edit!(LineEdit.edit_clear) == "" + @test edit!(edit_undo!) == "one two three" LineEdit.edit_move_left(s) LineEdit.edit_move_left(s) - LineEdit.edit_transpose_chars(s) - @test content(s) == "one two there" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_transpose_chars) == "one two there" + @test edit!(edit_undo!) == "one two three" LineEdit.move_line_start(s) - LineEdit.edit_kill_line(s) - @test content(s) == "" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_kill_line) == "" + @test edit!(edit_undo!) == "one two three" LineEdit.move_line_start(s) LineEdit.edit_kill_line(s) LineEdit.edit_yank(s) - LineEdit.edit_yank(s) - @test content(s) == "one two threeone two three" - LineEdit.pop_undo(s) - @test content(s) == "one two three" - LineEdit.pop_undo(s) - @test content(s) == "" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_yank) == "one two threeone two three" + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_undo!) == "" + @test edit!(edit_undo!) == "one two three" LineEdit.move_line_end(s) LineEdit.edit_backspace(s) LineEdit.edit_backspace(s) - LineEdit.edit_backspace(s) - @test content(s) == "one two th" - LineEdit.pop_undo(s) - @test content(s) == "one two thr" - LineEdit.pop_undo(s) - @test content(s) == "one two thre" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_backspace) == "one two th" + @test edit!(edit_undo!) == "one two thr" + @test edit!(edit_undo!) == "one two thre" + @test edit!(edit_undo!) == "one two three" LineEdit.push_undo(s) # TODO: incorporate push_undo into edit_splice! ? LineEdit.edit_splice!(s, 4 => 7, "stott") @test content(s) == "one stott three" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + s.last_action = :not_undo + @test edit!(edit_undo!) == "one two three" LineEdit.edit_move_left(s) LineEdit.edit_move_left(s) LineEdit.edit_move_left(s) - LineEdit.edit_delete(s) - @test content(s) == "one two thee" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_delete) == "one two thee" + @test edit!(edit_undo!) == "one two three" LineEdit.edit_move_word_left(s) LineEdit.edit_werase(s) - LineEdit.edit_delete_next_word(s) - @test content(s) == "one " - LineEdit.pop_undo(s) - @test content(s) == "one three" - LineEdit.pop_undo(s) - @test content(s) == "one two three" + @test edit!(LineEdit.edit_delete_next_word) == "one " + @test edit!(edit_undo!) == "one three" + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_redo!) == "one three" + @test edit!(edit_redo!) == "one " + @test edit!(edit_redo!) == "one " # nothing more to redo (this "beeps") + @test edit!(edit_undo!) == "one three" + @test edit!(edit_undo!) == "one two three" # pop initial insert of "one two three" - LineEdit.pop_undo(s) - @test content(s) == "" + @test edit!(edit_undo!) == "" + @test edit!(edit_undo!) == "" # nothing more to undo (this "beeps") end