Skip to content

Commit

Permalink
REPL/LineEdit to support "undo": part of JuliaLang#8447
Browse files Browse the repository at this point in the history
The REPL now supports undo via Ctrl-^ or Ctrl-_.

This should very closely minic the behavior of other readline/emacs
shells, except it doesn't let the user goto a historical entry (A),
edit it, goto a different historical entry (B), return to the first
(A) and undo the edits.
  • Loading branch information
srp committed Jan 4, 2015
1 parent 79c97ae commit dd44b25
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 14 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
==========================

Expand Down
69 changes: 56 additions & 13 deletions base/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type PromptState <: ModeState
terminal::TextTerminal
p::Prompt
input_buffer::IOBuffer
undo_buffers::Vector{IOBuffer}
ias::InputAreaState
indent::Int
end
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) &&
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}())
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion doc/manual/interacting-with-julia.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions test/lineedit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit dd44b25

Please sign in to comment.