Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPL/LineEdit to support "undo": part of #8447 #9596

Merged
merged 1 commit into from
Sep 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ Library improvements
* `Diagonal` is now parameterized on the type of the wrapped vector. This allows
for `Diagonal` matrices with arbitrary `AbstractVector`s ([#22718]).

* REPL Undo via Ctrl-/ and Ctrl-_

Compiler/Runtime improvements
-----------------------------

Expand Down
69 changes: 56 additions & 13 deletions base/repl/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ mutable struct PromptState <: ModeState
terminal::AbstractTerminal
p::Prompt
input_buffer::IOBuffer
undo_buffers::Vector{IOBuffer}
ias::InputAreaState
indent::Int
end
Expand Down Expand Up @@ -89,7 +90,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 @@ -148,16 +150,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 !isempty(p) && 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 @@ -440,10 +440,12 @@ function splice_buffer!(buf::IOBuffer, r::UnitRange{<:Integer}, ins::AbstractStr
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)
buf = s.input_buffer
function line_size()
p = position(buf)
Expand Down Expand Up @@ -476,9 +478,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 @@ -493,7 +497,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 @@ -511,7 +523,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 @@ -523,7 +536,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 @@ -535,15 +549,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you could push_undo only if !isempty(s.kill_buffer)?

edit_insert(buffer(s), s.kill_buffer)
refresh_line(s)
end

function edit_kill_line(s::MIState)
push_undo(s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

push_undo only if some characters are deleted?

buf = buffer(s)
pos = position(buf)
killbuf = readline(buf, chomp=false)
Expand All @@ -557,7 +574,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 @@ -572,15 +592,18 @@ end
edit_clear(buf::IOBuffer) = truncate(buf, 0)

function edit_clear(s::MIState)
push_undo(s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you could push_undo only if the buffer is not empty already? Another possibility could also be to not push a new state in push_undo when it's equal to the last pushed state?

edit_clear(buffer(s))
refresh_line(s)
end

function replace_line(s::PromptState, l::IOBuffer)
empty_undo(s)
s.input_buffer = copy(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 @@ -1120,8 +1143,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 @@ -1390,6 +1412,7 @@ AnyDict(
# Meta Enter
"\e\r" => (s,o...)->(edit_insert(s, '\n')),
"\e\n" => "\e\r",
"^_" => (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 @@ -1531,6 +1554,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 @@ -1559,7 +1583,9 @@ end

run_interface(::Prompt) = nothing

init_state(terminal, prompt::Prompt) = PromptState(terminal, prompt, IOBuffer(), InputAreaState(1, 1), #=indent(spaces)=#strwidth(prompt.prompt))
init_state(terminal, prompt::Prompt) =
PromptState(terminal, prompt, IOBuffer(), IOBuffer[], InputAreaState(1, 1),
#=indent(spaces)=#strwidth(prompt.prompt))

function init_state(terminal, m::ModalInterface)
s = MIState(m, m.modes[1], false, Dict{Any,Any}())
Expand Down Expand Up @@ -1592,6 +1618,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer the names of the new functions with an ending !, but it seems to be the style of the file, so better to keep as is for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this one is a good candidate for short-form function syntax.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably all be refactored to have !. It wasn't clear to me reading through this that it actually modified the PromptState until I saw the definition. The larger refactoring doesn't have to happen in this PR, but if you're adding a function now, might as well use the standard notation, IMO.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isempty(s.undo_buffers) && 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
1 change: 1 addition & 0 deletions doc/src/manual/interacting-with-julia.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ to do so).
| `^Y` | "Yank" insert the text from the kill buffer |
| `^T` | Transpose the characters about the cursor |
| `^Q` | Write a number in REPL and press `^Q` to open editor at corresponding stackframe or method |
| `^/`, `^_` | Undo |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see you add a key for ^/, how does it work? (when I tried to rebase last year, I had compilation problems with this key...)
Also, your commit message refers to "Ctrl-^", you should change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^/ isn't a valid control character (eg after subtracting 64 you get a negative number), however it appears that vt102 terminal emulators map it to emit ^_ for convience, see: https://apple.stackexchange.com/a/227286

From the terminals I tested on this is accurate that pressing ctrl-/ causes "^_" to appear in the console.



### 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 @@ -401,3 +401,96 @@ let
Base.LineEdit.InputAreaState(0,0), "julia> ", indent = 7)
@test s == Base.LineEdit.InputAreaState(3,1)
end

# test Undo
let
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use @testset ?

term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer())
s = LineEdit.init_state(term, ModalInterface([Prompt("test> ")]))
function bufferdata(s)
buf = LineEdit.buffer(s)
String(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