From 10ed407203a826f2a3df7731eb4b476ff407391b Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Tue, 5 Sep 2017 23:10:46 +0200 Subject: [PATCH 1/3] LineEdit: add convenient default mode in `state` function --- base/repl/LineEdit.jl | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index a2fa7e245e796..8efd54896c798 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -55,7 +55,7 @@ end MIState(i, c, a, m) = MIState(i, c, a, m, String[], 0, Char[], 0, :begin) function show(io::IO, s::MIState) - print(io, "MI State (", s.current_mode, " active)") + print(io, "MI State (", mode(s), " active)") end struct InputAreaState @@ -116,13 +116,13 @@ terminal(s::PromptState) = s.terminal for f in [:terminal, :on_enter, :add_history, :buffer, :(Base.isempty), :replace_line, :refresh_multi_line, :input_string, :update_display_buffer, :empty_undo, :push_undo, :pop_undo] - @eval ($f)(s::MIState, args...) = $(f)(s.mode_state[s.current_mode], args...) + @eval ($f)(s::MIState, args...) = $(f)(state(s), args...) end for f in [:edit_insert, :edit_insert_newline, :edit_backspace, :edit_move_left, :edit_move_right, :edit_move_word_left, :edit_move_word_right] @eval function ($f)(s::MIState, args...) - $(f)(s.mode_state[s.current_mode], args...) + $(f)(state(s), args...) return $(Expr(:quote, f)) end end @@ -172,7 +172,7 @@ end # Prompt Completions function complete_line(s::MIState) - complete_line(s.mode_state[s.current_mode], s.key_repeats) + complete_line(state(s), s.key_repeats) refresh_line(s) :complete_line end @@ -1352,8 +1352,8 @@ function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState) s.ias = refresh_multi_line(termbuf, s.terminal, buf, s.ias, s.backward ? "(reverse-i-search)`" : "(forward-i-search)`") end -state(s::MIState, p) = s.mode_state[p] -state(s::PromptState, p) = (@assert s.p == p; s) +state(s::MIState, p=mode(s)) = s.mode_state[p] +state(s::PromptState, p=mode(s)) = (@assert s.p == p; s) mode(s::MIState) = s.current_mode mode(s::PromptState) = s.p mode(s::SearchState) = @assert false @@ -1791,31 +1791,32 @@ function activate(p::TextInterface, s::ModeState, termbuf, term::TextTerminal) end function activate(p::TextInterface, s::MIState, termbuf, term::TextTerminal) - @assert p == s.current_mode - activate(p, s.mode_state[s.current_mode], termbuf, term) + @assert p == mode(s) + activate(p, state(s), termbuf, term) end activate(m::ModalInterface, s::MIState, termbuf, term::TextTerminal) = - activate(s.current_mode, s, termbuf, term) + activate(mode(s), s, termbuf, term) commit_changes(t::UnixTerminal, termbuf) = write(t, take!(termbuf.out_stream)) -function transition(f::Function, s::MIState, mode) - if mode === :abort + +function transition(f::Function, s::MIState, newmode) + if newmode === :abort s.aborted = true return end - if mode === :reset + if newmode === :reset reset_state(s) return end - if !haskey(s.mode_state,mode) - s.mode_state[mode] = init_state(terminal(s), mode) + if !haskey(s.mode_state, newmode) + s.mode_state[newmode] = init_state(terminal(s), newmode) end termbuf = TerminalBuffer(IOBuffer()) t = terminal(s) - s.mode_state[s.current_mode] = deactivate(s.current_mode, s.mode_state[s.current_mode], termbuf, t) - s.current_mode = mode + s.mode_state[mode(s)] = deactivate(mode(s), state(s), termbuf, t) + s.current_mode = newmode f() - activate(mode, s.mode_state[mode], termbuf, t) + activate(newmode, state(s, newmode), termbuf, t) commit_changes(t, termbuf) end transition(s::MIState, mode) = transition((args...)->nothing, s, mode) @@ -1830,7 +1831,7 @@ function reset_state(s::PromptState) end function reset_state(s::MIState) - for (mode,state) in s.mode_state + for (mode, state) in s.mode_state reset_state(state) end end @@ -1878,7 +1879,7 @@ function run_interface(terminal, m::ModalInterface) Expr(:body, Expr(:return, Expr(:call, - QuoteNode(mode(state(s, s.current_mode)).on_done), + QuoteNode(mode(state(s)).on_done), QuoteNode(s), QuoteNode(buf), QuoteNode(ok))))) @@ -1911,8 +1912,8 @@ pop_undo(s) = nothing keymap(s::PromptState, prompt::Prompt) = prompt.keymap_dict keymap_data(s::PromptState, prompt::Prompt) = prompt.keymap_func_data -keymap(ms::MIState, m::ModalInterface) = keymap(ms.mode_state[ms.current_mode], ms.current_mode) -keymap_data(ms::MIState, m::ModalInterface) = keymap_data(ms.mode_state[ms.current_mode], ms.current_mode) +keymap(ms::MIState, m::ModalInterface) = keymap(state(ms), mode(ms)) +keymap_data(ms::MIState, m::ModalInterface) = keymap_data(state(ms), mode(ms)) function prompt!(term, prompt, s = init_state(term, prompt)) Base.reseteof(term) From 5f708e8788c09fecae955c446792f82fc10bc502 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 3 Sep 2017 23:15:38 +0200 Subject: [PATCH 2/3] REPL: enable "redo" after "undo" --- base/repl/LineEdit.jl | 55 +++++++++++++++++++--- test/lineedit.jl | 106 ++++++++++++++++++++---------------------- 2 files changed, 98 insertions(+), 63 deletions(-) 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 From 34df23e8abf91f82cec717494dcb5929d5124459 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Thu, 7 Sep 2017 10:00:57 +0200 Subject: [PATCH 3/3] REPL undo: fix edit_kill_region and add more tests --- base/repl/LineEdit.jl | 10 +++++++--- test/lineedit.jl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 1e1a8a9fcde47..95a05868bce22 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -726,10 +726,14 @@ function edit_copy_region(s::MIState) end function edit_kill_region(s::MIState) - push_kill!(s, edit_splice!(s)) || return :ignore push_undo(s) - refresh_line(s) - :edit_kill_region + if push_kill!(s, edit_splice!(s)) + refresh_line(s) + :edit_kill_region + else + pop_undo(s) + :ignore + end end function edit_transpose_chars(s::MIState) diff --git a/test/lineedit.jl b/test/lineedit.jl index 8dc8d5aad2a53..e6b50cb14e402 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -644,10 +644,15 @@ end @test edit!(LineEdit.edit_clear) == "" @test edit!(edit_undo!) == "one two three" + @test edit!(LineEdit.edit_insert_newline) == "one two three\n" + @test edit!(edit_undo!) == "one two three" + LineEdit.edit_move_left(s) LineEdit.edit_move_left(s) @test edit!(LineEdit.edit_transpose_chars) == "one two there" @test edit!(edit_undo!) == "one two three" + @test edit!(LineEdit.edit_transpose_words) == "one three two" + @test edit!(edit_undo!) == "one two three" LineEdit.move_line_start(s) @test edit!(LineEdit.edit_kill_line) == "" @@ -661,6 +666,15 @@ end @test edit!(edit_undo!) == "" @test edit!(edit_undo!) == "one two three" + LineEdit.setmark(s) + LineEdit.edit_move_word_right(s) + @test edit!(LineEdit.edit_kill_region) == " two three" + @test edit!(LineEdit.edit_yank) == "one two three" + @test edit!(LineEdit.edit_yank_pop) == "one two three two three" + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_undo!) == " two three" + @test edit!(edit_undo!) == "one two three" + LineEdit.move_line_end(s) LineEdit.edit_backspace(s) LineEdit.edit_backspace(s) @@ -692,6 +706,22 @@ end @test edit!(edit_undo!) == "one three" @test edit!(edit_undo!) == "one two three" + LineEdit.move_line_start(s) + @test edit!(LineEdit.edit_upper_case) == "ONE two three" + LineEdit.move_line_start(s) + @test edit!(LineEdit.edit_lower_case) == "one two three" + @test edit!(LineEdit.edit_title_case) == "one Two three" + @test edit!(edit_undo!) == "one two three" + @test edit!(edit_undo!) == "ONE two three" + @test edit!(edit_undo!) == "one two three" + + LineEdit.move_line_end(s) + edit_insert(s, " ") + @test edit!(LineEdit.edit_tab) == "one two three " + @test edit!(edit_undo!) == "one two three " + @test edit!(edit_undo!) == "one two three" + # TODO: add tests for complete_line, which don't work directly + # pop initial insert of "one two three" @test edit!(edit_undo!) == "" @test edit!(edit_undo!) == "" # nothing more to undo (this "beeps")