diff --git a/NEWS.md b/NEWS.md index 39810e08535d4..4d4de48c603da 100644 --- a/NEWS.md +++ b/NEWS.md @@ -168,6 +168,11 @@ Deprecated or removed * `randbool` is deprecated. Use `rand(Bool)` to produce a random boolean value, and `bitrand` to produce a random BitArray ([#9105], [#9569]). +REPL improvements +----------------- + + * Undo via Ctrl-/ and Ctrl-_ + Julia v0.3.0 Release Notes ========================== diff --git a/base/LineEdit.jl b/base/LineEdit.jl index 0fb95c1908133..9552abdc49fc2 100644 --- a/base/LineEdit.jl +++ b/base/LineEdit.jl @@ -55,6 +55,7 @@ type PromptState <: ModeState terminal::TextTerminal p::Prompt input_buffer::IOBuffer + undo_buffers::Vector{IOBuffer} ias::InputAreaState indent::Int end @@ -87,7 +88,8 @@ terminal(s::PromptState) = s.terminal for f in [:terminal, :edit_insert, :on_enter, :add_history, :buffer, :edit_backspace, :(Base.isempty), :replace_line, :refresh_multi_line, :input_string, :edit_move_left, :edit_move_right, - :edit_move_word_left, :edit_move_word_right, :update_display_buffer] + :edit_move_word_left, :edit_move_word_right, :update_display_buffer, + :empty_undo, :push_undo, :pop_undo] @eval ($f)(s::MIState, args...) = $(f)(s.mode_state[s.current_mode], args...) end @@ -146,16 +148,14 @@ function complete_line(s::PromptState, repeats) elseif length(completions) == 1 # Replace word by completion prev_pos = position(s.input_buffer) - seek(s.input_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.input_buffer), prev_pos, completions[1]) + edit_replace(s, prev_pos-sizeof(partial), prev_pos, completions[1]) else p = common_prefix(completions) if length(p) > 0 && p != partial # All possible completions share the same prefix, so we might as # well complete that prev_pos = position(s.input_buffer) - seek(s.input_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.input_buffer), prev_pos, p) + edit_replace(s, prev_pos-sizeof(partial), prev_pos, p) elseif repeats > 0 show_completions(s, completions) end @@ -479,10 +479,12 @@ function splice_buffer!{T<:Integer}(buf::IOBuffer, r::UnitRange{T}, ins::Abstrac end function edit_replace(s, from, to, str) + push_undo(s) splice_buffer!(buffer(s), from:to-1, str) end function edit_insert(s::PromptState, c) + push_undo(s) str = string(c) edit_insert(s.input_buffer, str) if !('\n' in str) && eof(s.input_buffer) && @@ -503,9 +505,11 @@ function edit_insert(buf::IOBuffer, c) end function edit_backspace(s::PromptState) + push_undo(s) if edit_backspace(s.input_buffer) refresh_line(s) else + pop_undo(s) beep(terminal(s)) end end @@ -520,7 +524,15 @@ function edit_backspace(buf::IOBuffer) end end -edit_delete(s) = edit_delete(buffer(s)) ? refresh_line(s) : beep(terminal(s)) +function edit_delete(s) + push_undo(s) + if edit_delete(buffer(s)) + refresh_line(s) + else + pop_undo(s) + beep(terminal(s)) + end +end function edit_delete(buf::IOBuffer) eof(buf) && return false oldpos = position(buf) @@ -538,7 +550,8 @@ function edit_werase(buf::IOBuffer) true end function edit_werase(s) - edit_werase(buffer(s)) && refresh_line(s) + push_undo(s) + edit_werase(buffer(s)) ? refresh_line(s) : pop_undo(s) end function edit_delete_prev_word(buf::IOBuffer) @@ -550,7 +563,8 @@ function edit_delete_prev_word(buf::IOBuffer) true end function edit_delete_prev_word(s) - edit_delete_prev_word(buffer(s)) && refresh_line(s) + push_undo(s) + edit_delete_prev_word(buffer(s)) ? refresh_line(s) : pop_undo(s) end function edit_delete_next_word(buf::IOBuffer) @@ -562,15 +576,18 @@ function edit_delete_next_word(buf::IOBuffer) true end function edit_delete_next_word(s) - edit_delete_next_word(buffer(s)) && refresh_line(s) + push_undo(s) + edit_delete_next_word(buffer(s)) ? refresh_line(s) : pop_undo(s) end function edit_yank(s::MIState) + push_undo(s) edit_insert(buffer(s), s.kill_buffer) refresh_line(s) end function edit_kill_line(s::MIState) + push_undo(s) buf = buffer(s) pos = position(buf) killbuf = readline(buf) @@ -584,7 +601,10 @@ function edit_kill_line(s::MIState) refresh_line(s) end -edit_transpose(s) = edit_transpose(buffer(s)) && refresh_line(s) +function edit_transpose(s) + push_undo(s) + edit_transpose(buffer(s)) ? refresh_line(s) : pop_undo(s) +end function edit_transpose(buf::IOBuffer) position(buf) == 0 && return false eof(buf) && char_move_left(buf) @@ -599,15 +619,18 @@ end edit_clear(buf::IOBuffer) = truncate(buf, 0) function edit_clear(s::MIState) + push_undo(s) edit_clear(buffer(s)) refresh_line(s) end function replace_line(s::PromptState, l::IOBuffer) + empty_undo(s) s.input_buffer = l end function replace_line(s::PromptState, l) + empty_undo(s) s.input_buffer.ptr = 1 s.input_buffer.size = 0 write(s.input_buffer, l) @@ -1112,8 +1135,7 @@ function complete_line(s::SearchState, repeats) # For now only allow exact completions in search mode if length(completions) == 1 prev_pos = position(s.query_buffer) - seek(s.query_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.query_buffer), prev_pos, completions[1]) + edit_replace(s, prev_pos-sizeof(partial), prev_pos, completions[1]) end end @@ -1345,6 +1367,9 @@ AnyDict( # Meta Enter "\e\r" => (s,o...)->(edit_insert(s, '\n')), "\e\n" => "\e\r", + # Undo: Ctrl-/ or Ctrl-_ + "^/" => (s,o...)->(pop_undo(s) ? refresh_line(s) : beep(terminal(s))), + "^_" => "^/", # Simply insert it into the buffer by default "*" => (s,data,c)->(edit_insert(s, c)), "^U" => (s,o...)->edit_clear(s), @@ -1483,6 +1508,7 @@ function reset_state(s::PromptState) s.input_buffer.size = 0 s.input_buffer.ptr = 1 end + empty_undo(s) s.ias = InputAreaState(0, 0) end @@ -1512,7 +1538,7 @@ end run_interface(::Prompt) = nothing -init_state(terminal, prompt::Prompt) = PromptState(terminal, prompt, IOBuffer(), InputAreaState(1, 1), length(prompt.prompt)) +init_state(terminal, prompt::Prompt) = PromptState(terminal, prompt, IOBuffer(), (IOBuffer)[], InputAreaState(1, 1), length(prompt.prompt)) function init_state(terminal, m::ModalInterface) s = MIState(m, m.modes[1], false, Dict{Any,Any}()) @@ -1539,6 +1565,23 @@ buffer(s::PromptState) = s.input_buffer buffer(s::SearchState) = s.query_buffer buffer(s::PrefixSearchState) = s.response_buffer +function empty_undo(s::PromptState) + empty!(s.undo_buffers) +end +empty_undo(s) = nothing + +function push_undo(s::PromptState) + push!(s.undo_buffers, copy(s.input_buffer)) +end +push_undo(s) = nothing + +function pop_undo(s::PromptState) + length(s.undo_buffers) > 0 || return false + s.input_buffer = pop!(s.undo_buffers) + true +end +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) diff --git a/doc/manual/interacting-with-julia.rst b/doc/manual/interacting-with-julia.rst index f70ae683f2089..3326542e2f270 100644 --- a/doc/manual/interacting-with-julia.rst +++ b/doc/manual/interacting-with-julia.rst @@ -159,7 +159,7 @@ The Julia REPL makes great use of key bindings. Several control-key bindings we +------------------------+----------------------------------------------------+ | ``^T`` | Transpose the characters about the cursor | +------------------------+----------------------------------------------------+ -| Delete, ``^D`` | Forward delete one character (when buffer has text)| +| ``^/``, ``^_`` | Undo | +------------------------+----------------------------------------------------+ Customizing keybindings diff --git a/test/lineedit.jl b/test/lineedit.jl index 0e12a0d2326c6..441024fe4f720 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -395,3 +395,96 @@ term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer()) s = LineEdit.refresh_multi_line(termbuf, term, buf, Base.LineEdit.InputAreaState(0,0), "julia> ", indent = 7) @test s == Base.LineEdit.InputAreaState(3,1) + +# test Undo +let + term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer()) + s = LineEdit.init_state(term, ModalInterface([Prompt("test> ")])) + function bufferdata(s) + buf = LineEdit.buffer(s) + bytestring(buf.data[1:buf.size]) + end + + LineEdit.edit_insert(s, "one two three") + + LineEdit.edit_delete_prev_word(s) + @test bufferdata(s) == "one two " + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_insert(s, " four") + LineEdit.edit_insert(s, " five") + @test bufferdata(s) == "one two three four five" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three four" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_clear(s) + @test bufferdata(s) == "" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_move_left(s) + LineEdit.edit_move_left(s) + LineEdit.edit_transpose(s) + @test bufferdata(s) == "one two there" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.move_line_start(s) + LineEdit.edit_kill_line(s) + @test bufferdata(s) == "" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.move_line_start(s) + LineEdit.edit_kill_line(s) + LineEdit.edit_yank(s) + LineEdit.edit_yank(s) + @test bufferdata(s) == "one two threeone two three" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + LineEdit.pop_undo(s) + @test bufferdata(s) == "" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.move_line_end(s) + LineEdit.edit_backspace(s) + LineEdit.edit_backspace(s) + LineEdit.edit_backspace(s) + @test bufferdata(s) == "one two th" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two thr" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two thre" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_replace(s, 4, 7, "stott") + @test bufferdata(s) == "one stott three" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_move_left(s) + LineEdit.edit_move_left(s) + LineEdit.edit_move_left(s) + LineEdit.edit_delete(s) + @test bufferdata(s) == "one two thee" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + LineEdit.edit_move_word_left(s) + LineEdit.edit_werase(s) + LineEdit.edit_delete_next_word(s) + @test bufferdata(s) == "one " + LineEdit.pop_undo(s) + @test bufferdata(s) == "one three" + LineEdit.pop_undo(s) + @test bufferdata(s) == "one two three" + + # pop initial insert of "one two three" + LineEdit.pop_undo(s) + @test bufferdata(s) == "" +end