-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,7 @@ mutable struct PromptState <: ModeState | |
terminal::AbstractTerminal | ||
p::Prompt | ||
input_buffer::IOBuffer | ||
undo_buffers::Vector{IOBuffer} | ||
ias::InputAreaState | ||
indent::Int | ||
end | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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) | ||
|
@@ -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 | ||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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) | ||
edit_insert(buffer(s), s.kill_buffer) | ||
refresh_line(s) | ||
end | ||
|
||
function edit_kill_line(s::MIState) | ||
push_undo(s) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
buf = buffer(s) | ||
pos = position(buf) | ||
killbuf = readline(buf, chomp=false) | ||
|
@@ -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) | ||
|
@@ -572,15 +592,18 @@ end | |
edit_clear(buf::IOBuffer) = truncate(buf, 0) | ||
|
||
function edit_clear(s::MIState) | ||
push_undo(s) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe you could |
||
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) | ||
|
@@ -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 | ||
|
||
|
@@ -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), | ||
|
@@ -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 | ||
|
||
|
@@ -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}()) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer the names of the new functions with an ending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, this one is a good candidate for short-form function syntax. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should probably all be refactored to have |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't see you add a key for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -401,3 +401,96 @@ let | |
Base.LineEdit.InputAreaState(0,0), "julia> ", indent = 7) | ||
@test s == Base.LineEdit.InputAreaState(3,1) | ||
end | ||
|
||
# test Undo | ||
let | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
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 |
There was a problem hiding this comment.
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)
?