From dd7feaeac0a0ce68b6b25bc5a026ef2723076e9c Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 20 Aug 2017 18:17:34 +0200 Subject: [PATCH 1/6] REPL: free-up '\0' as a key (sent by Ctrl-Space) '\0' was used as a sentinel for self-insert (spelled "*" in keymaps), which made it un-available for Ctrl-Space combo. We replace here '\0' with an un-assigned sentinel Char value. --- base/repl/LineEdit.jl | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index f29956839b0da..f87117513cee0 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -731,16 +731,18 @@ end ### Keymap Support +const wildcard = Char(0x0010f7ff) # "Private Use" Char + normalize_key(key::Char) = string(key) normalize_key(key::Integer) = normalize_key(Char(key)) function normalize_key(key::AbstractString) - '\0' in key && error("Matching \\0 not currently supported.") + wildcard in key && error("Matching Char(0x0010f7ff) not supported.") buf = IOBuffer() i = start(key) while !done(key, i) c, i = next(key, i) if c == '*' - write(buf, '\0') + write(buf, wildcard) elseif c == '^' c, i = next(key, i) write(buf, uppercase(c)-64) @@ -810,19 +812,25 @@ struct KeyAlias KeyAlias(seq) = new(normalize_key(seq)) end -match_input(k::Function, s, term, cs, keymap) = (update_key_repeats(s, cs); return keymap_fcn(k, String(cs))) +function match_input(k::Function, s, term, cs, keymap) + update_key_repeats(s, cs) + return keymap_fcn(k, String(cs)) +end + match_input(k::Void, s, term, cs, keymap) = (s,p) -> return :ok -match_input(k::KeyAlias, s, term, cs, keymap) = match_input(keymap, s, IOBuffer(k.seq), Char[], keymap) +match_input(k::KeyAlias, s, term, cs, keymap) = + match_input(keymap, s, IOBuffer(k.seq), Char[], keymap) + function match_input(k::Dict, s, term=terminal(s), cs=Char[], keymap = k) # if we run out of characters to match before resolving an action, # return an empty keymap function eof(term) && return keymap_fcn(nothing, "") c = read(term, Char) - # Ignore any '\0' (eg, CTRL-space in xterm), as this is used as a + # Ignore any `wildcard` as this is used as a # placeholder for the wildcard (see normalize_key("*")) - c != '\0' || return keymap_fcn(nothing, "") + c == wildcard && return keymap_fcn(nothing, "") push!(cs, c) - key = haskey(k, c) ? c : '\0' + key = haskey(k, c) ? c : wildcard # if we don't match on the key, look for a default action then fallback on 'nothing' to ignore return match_input(get(k, key, nothing), s, term, cs, keymap) end @@ -913,12 +921,12 @@ function fixup_keymaps!(dict::Dict, level, s, subkeymap) end function add_specialisations(dict, subdict, level) - default_branch = subdict['\0'] + default_branch = subdict[wildcard] if isa(default_branch, Dict) # Go through all the keymaps in the default branch # and copy them over to dict for s in keys(default_branch) - s == '\0' && add_specialisations(dict, default_branch, level+1) + s == wildcard && add_specialisations(dict, default_branch, level+1) fixup_keymaps!(dict, level, s, default_branch[s]) end end @@ -927,11 +935,11 @@ end postprocess!(others) = nothing function postprocess!(dict::Dict) # needs to be done first for every branch - if haskey(dict, '\0') + if haskey(dict, wildcard) add_specialisations(dict, dict, 1) end for (k,v) in dict - k == '\0' && continue + k == wildcard && continue postprocess!(v) end end @@ -1015,7 +1023,7 @@ function keymap(keymaps::Array{<:Dict}) end const escape_defaults = merge!( - AnyDict(Char(i) => nothing for i=vcat(1:26, 28:31)), # Ignore control characters by default + AnyDict(Char(i) => nothing for i=vcat(0:26, 28:31)), # Ignore control characters by default AnyDict( # And ignore other escape sequences by default "\e*" => nothing, "\e[*" => nothing, From adae8305403c97ee5563a25b5f55ab73616a5a04 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 20 Aug 2017 10:26:18 +0200 Subject: [PATCH 2/6] REPL: add few "region" operations * add possibility to set the "mark" in the edit area, with C-Space * the mark and current position (the "point") delimit a region * C-x C-x exchanges the mark with the point * M-w is set to copy the region into the kill buffer * M-W is set to kill the region into the kill buffer (in Emacs, the binding for this is C-w, but it's already taken by edit_werase) --- base/repl/LineEdit.jl | 79 +++++++++++++++++++----- doc/src/manual/interacting-with-julia.md | 12 ++-- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index f87117513cee0..709338dc9d604 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -67,6 +67,19 @@ mutable struct PromptState <: ModeState indent::Int end +setmark(s) = mark(buffer(s)) + +# the default mark is 0 +getmark(s) = max(0, buffer(s).mark) + +# given two buffer positions delimitating a region +# as an close-open range, return a range of the included +# positions, 0-based (suitable for splice_buffer!) +region(a::Int, b::Int) = ((a, b) = minmax(a, b); a:b-1) +region(s) = region(getmark(s), position(buffer(s))) + +const REGION_ANIMATION_DURATION = Ref(0.2) + input_string(s::PromptState) = String(take!(copy(s.input_buffer))) input_string_newlines(s::PromptState) = count(c->(c == '\n'), input_string(s)) @@ -287,6 +300,17 @@ function reset_key_repeats(f::Function, s::MIState) end end +edit_exchange_point_and_mark(s::MIState) = + edit_exchange_point_and_mark(buffer(s)) && (refresh_line(s); true) + +function edit_exchange_point_and_mark(buf::IOBuffer) + m = getmark(buf) + m == position(buf) && return false + mark(buf) + seek(buf, m) + true +end + char_move_left(s::PromptState) = char_move_left(s.input_buffer) function char_move_left(buf::IOBuffer) while position(buf) > 0 @@ -431,16 +455,23 @@ end # splice! for IOBuffer: convert from 0-indexed positions, update the size, # and keep the cursor position stable with the text +# returns the removed portion as a String function splice_buffer!(buf::IOBuffer, r::UnitRange{<:Integer}, ins::AbstractString = "") pos = position(buf) - if !isempty(r) && pos in r + if pos in r seek(buf, first(r)) elseif pos > last(r) seek(buf, pos - length(r)) end - splice!(buf.data, r + 1, Vector{UInt8}(ins)) # position(), etc, are 0-indexed + if first(r) < buf.mark <= last(r) + unmark(buf) + elseif buf.mark > last(r) && !isempty(r) + buf.mark += sizeof(ins) - length(r) + end + ret = splice!(buf.data, r + 1, Vector{UInt8}(ins)) # position(), etc, are 0-indexed buf.size = buf.size + sizeof(ins) - length(r) seek(buf, position(buf) + sizeof(ins)) + String(ret) end function edit_replace(s, from, to, str) @@ -597,6 +628,26 @@ function edit_kill_line(s::MIState) refresh_line(s) end +function edit_copy_region(s::MIState) + buf = buffer(s) + if edit_exchange_point_and_mark(buf) # region non-empty + s.kill_buffer = String(buf.data[region(buf)+1]) + if REGION_ANIMATION_DURATION[] > 0.0 + refresh_line(s) + sleep(REGION_ANIMATION_DURATION[]) + end + edit_exchange_point_and_mark(s) # includes refresh_line + end +end + +function edit_kill_region(s::MIState) + reg = region(s) + if !isempty(reg) + s.kill_buffer = splice_buffer!(buffer(s), reg) + refresh_line(s) + end +end + edit_transpose_chars(s) = edit_transpose_chars(buffer(s)) && refresh_line(s) function edit_transpose_chars(buf::IOBuffer) @@ -784,20 +835,14 @@ function add_nested_key!(keymap::Dict, key, value; override = false) i = start(key) while !done(key, i) c, i = next(key, i) - if c in keys(keymap) - if done(key, i) && override - # isa(keymap[c], Dict) - In this case we're overriding a prefix of an existing command - keymap[c] = value - break - else - if !isa(keymap[c], Dict) - error("Conflicting definitions for keyseq " * escape_string(key) * " within one keymap") - end - end - elseif done(key, i) + if !override && c in keys(keymap) && (done(key, i) || !isa(keymap[c], Dict)) + error("Conflicting definitions for keyseq " * escape_string(key) * + " within one keymap") + end + if done(key, i) keymap[c] = value break - else + elseif !(c in keys(keymap) && isa(keymap[c], Dict)) keymap[c] = Dict{Char,Any}() end keymap = keymap[c] @@ -1499,6 +1544,9 @@ AnyDict( return :abort end end, + # Ctrl-Space + "\0" => (s,o...)->setmark(s), + "^X^X" => (s,o...)->edit_exchange_point_and_mark(s), "^B" => (s,o...)->edit_move_left(s), "^F" => (s,o...)->edit_move_right(s), # Meta B @@ -1521,6 +1569,8 @@ AnyDict( "^U" => (s,o...)->edit_clear(s), "^K" => (s,o...)->edit_kill_line(s), "^Y" => (s,o...)->edit_yank(s), + "\ew" => (s,o...)->edit_copy_region(s), + "\eW" => (s,o...)->edit_kill_region(s), "^A" => (s,o...)->(move_line_start(s); refresh_line(s)), "^E" => (s,o...)->(move_line_end(s); refresh_line(s)), # Try to catch all Home/End keys @@ -1725,6 +1775,7 @@ end buffer(s::PromptState) = s.input_buffer buffer(s::SearchState) = s.query_buffer buffer(s::PrefixSearchState) = s.response_buffer +buffer(s::IOBuffer) = s keymap(s::PromptState, prompt::Prompt) = prompt.keymap_dict keymap_data(s::PromptState, prompt::Prompt) = prompt.keymap_func_data diff --git a/doc/src/manual/interacting-with-julia.md b/doc/src/manual/interacting-with-julia.md index 03653d2cfeaf8..45b1129881227 100644 --- a/doc/src/manual/interacting-with-julia.md +++ b/doc/src/manual/interacting-with-julia.md @@ -160,17 +160,21 @@ to do so). | Down arrow | Move down one line (or to the next history entry) | | Page-up | Change to the previous history entry that matches the text before the cursor | | Page-down | Change to the next history entry that matches the text before the cursor | -| `meta-F` | Move right one word | -| `meta-B` | Move left one word | +| `meta-f` | Move right one word | +| `meta-b` | Move left one word | | `meta-<` | Change to the first history entry (of the current session if it is before the current position in history) | | `meta->` | Change to the last history entry | +| `^-Space` | Set the "mark" in the editing region | +| `^X^X` | Exchange the current position with the mark | | **Editing** |   | | Backspace, `^H` | Delete the previous character | | Delete, `^D` | Forward delete one character (when buffer has text) | | meta-Backspace | Delete the previous word | -| `meta-D` | Forward delete the next word | +| `meta-d` | Forward delete the next word | | `^W` | Delete previous text up to the nearest whitespace | -| `^K` | "Kill" to end of line, placing the text in a buffer | +| `meta-w` | Copy the current region in the kill buffer | +| `meta-W` | "Kill" the current region, placed the text in the kill buffer | +| `^K` | "Kill" to end of line, placing the text in the kill buffer | | `^Y` | "Yank" insert the text from the kill buffer | | `^T` | Transpose the characters about the cursor | | `meta-u` | Change the next word to uppercase | From 9cf7505c8b972da25c1b554225db2214255a6589 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sat, 19 Aug 2017 16:12:28 +0200 Subject: [PATCH 3/6] REPL: register previous run action as symbol --- base/repl/LineEdit.jl | 109 ++++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 709338dc9d604..97ea953148a88 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -43,10 +43,11 @@ mutable struct MIState aborted::Bool mode_state::Dict kill_buffer::String - previous_key::Array{Char,1} + previous_key::Vector{Char} key_repeats::Int + last_action::Symbol end -MIState(i, c, a, m) = MIState(i, c, a, m, "", Char[], 0) +MIState(i, c, a, m) = MIState(i, c, a, m, "", Char[], 0, :begin) function show(io::IO, s::MIState) print(io, "MI State (", s.current_mode, " active)") @@ -103,13 +104,20 @@ complete_line(c::EmptyCompletionProvider, s) = [], true, true terminal(s::IO) = s terminal(s::PromptState) = s.terminal -for f in [:terminal, :edit_insert, :edit_insert_newline, :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] +for f in [:terminal, :on_enter, :add_history, :buffer, :(Base.isempty), + :replace_line, :refresh_multi_line, :input_string, :update_display_buffer] @eval ($f)(s::MIState, args...) = $(f)(s.mode_state[s.current_mode], 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...) + return $(Expr(:quote, f)) + end +end + + function common_prefix(completions) ret = "" c1 = completions[1] @@ -153,7 +161,12 @@ function show_completions(s::PromptState, completions) end # Prompt Completions -complete_line(s::MIState) = complete_line(s.mode_state[s.current_mode], s.key_repeats) +function complete_line(s::MIState) + complete_line(s.mode_state[s.current_mode], s.key_repeats) + refresh_line(s) + :complete_line +end + function complete_line(s::PromptState, repeats) completions, partial, should_complete = complete_line(s.p.complete, s) if isempty(completions) @@ -564,7 +577,9 @@ function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=align) return true end -edit_delete(s) = edit_delete(buffer(s)) ? refresh_line(s) : beep(terminal(s)) +edit_delete(s::MIState) = (edit_delete(buffer(s)) ? refresh_line(s) : beep(terminal(s)); + :edit_delete) + function edit_delete(buf::IOBuffer) eof(buf) && return false oldpos = position(buf) @@ -581,9 +596,7 @@ function edit_werase(buf::IOBuffer) splice_buffer!(buf, pos0:pos1-1) true end -function edit_werase(s) - edit_werase(buffer(s)) && refresh_line(s) -end +edit_werase(s::MIState) = (edit_werase(buffer(s)) && refresh_line(s); :edit_werase) function edit_delete_prev_word(buf::IOBuffer) pos1 = position(buf) @@ -593,8 +606,9 @@ function edit_delete_prev_word(buf::IOBuffer) splice_buffer!(buf, pos0:pos1-1) true end -function edit_delete_prev_word(s) +function edit_delete_prev_word(s::MIState) edit_delete_prev_word(buffer(s)) && refresh_line(s) + :edit_delete_prev_word end function edit_delete_next_word(buf::IOBuffer) @@ -605,13 +619,16 @@ function edit_delete_next_word(buf::IOBuffer) splice_buffer!(buf, pos0:pos1-1) true end + function edit_delete_next_word(s) edit_delete_next_word(buffer(s)) && refresh_line(s) + :edit_delete_next_word end function edit_yank(s::MIState) edit_insert(buffer(s), s.kill_buffer) refresh_line(s) + :edit_yank end function edit_kill_line(s::MIState) @@ -622,10 +639,14 @@ function edit_kill_line(s::MIState) killbuf = killbuf[1:end-1] char_move_left(buf) end - s.kill_buffer = s.key_repeats > 0 ? s.kill_buffer * killbuf : killbuf - - splice_buffer!(buf, pos:position(buf)-1) - refresh_line(s) + if !isempty(killbuf) + s.kill_buffer = s.key_repeats > 0 ? s.kill_buffer * killbuf : killbuf + splice_buffer!(buf, pos:position(buf)-1) + refresh_line(s) + :edit_kill_line + else + :ignore + end end function edit_copy_region(s::MIState) @@ -637,6 +658,9 @@ function edit_copy_region(s::MIState) sleep(REGION_ANIMATION_DURATION[]) end edit_exchange_point_and_mark(s) # includes refresh_line + :edit_copy_region + else + :ignore end end @@ -645,10 +669,16 @@ function edit_kill_region(s::MIState) if !isempty(reg) s.kill_buffer = splice_buffer!(buffer(s), reg) refresh_line(s) + :edit_kill_region + else + :ignore end end -edit_transpose_chars(s) = edit_transpose_chars(buffer(s)) && refresh_line(s) +function edit_transpose_chars(s::MIState) + edit_transpose_chars(buffer(s)) && refresh_line(s) + :edit_transpose +end function edit_transpose_chars(buf::IOBuffer) position(buf) == 0 && return false @@ -661,7 +691,10 @@ function edit_transpose_chars(buf::IOBuffer) return true end -edit_transpose_words(s) = edit_transpose_words(buffer(s)) && refresh_line(s) +function edit_transpose_words(s) + edit_transpose_words(buffer(s)) && refresh_line(s) + :edit_transpose_words +end function edit_transpose_words(buf::IOBuffer, mode=:emacs) mode in [:readline, :emacs] || @@ -687,9 +720,9 @@ function edit_transpose_words(buf::IOBuffer, mode=:emacs) end -edit_upper_case(s) = edit_replace_word_right(s, uppercase) -edit_lower_case(s) = edit_replace_word_right(s, lowercase) -edit_title_case(s) = edit_replace_word_right(s, ucfirst) +edit_upper_case(s) = (edit_replace_word_right(s, uppercase); :edit_upper_case) +edit_lower_case(s) = (edit_replace_word_right(s, lowercase); :edit_lower_case) +edit_title_case(s) = (edit_replace_word_right(s, ucfirst); :edit_title_case) edit_replace_word_right(s, replace::Function) = edit_replace_word_right(buffer(s), replace) && refresh_line(s) @@ -711,6 +744,7 @@ edit_clear(buf::IOBuffer) = truncate(buf, 0) function edit_clear(s::MIState) edit_clear(buffer(s)) refresh_line(s) + :edit_clear end function replace_line(s::PromptState, l::IOBuffer) @@ -1429,11 +1463,13 @@ function move_line_start(s::MIState) else seek(buf, rsearch(buf.data, '\n', curpos)) end + :move_line_start end function move_line_end(s::MIState) s.key_repeats > 0 ? move_input_end(s) : move_line_end(buffer(s)) + :move_line_end end function move_line_end(buf::IOBuffer) eof(buf) && return @@ -1491,14 +1527,17 @@ end # jump_spaces: if cursor is on a ' ', move it to the first non-' ' char on the right # if `delete_trailing`, ignore trailing ' ' by deleting them -function edit_tab(s, jump_spaces=false, delete_trailing=jump_spaces) - tab_should_complete(s) ? - complete_line(s) : - edit_tab(buffer(s), jump_spaces, delete_trailing) - refresh_line(s) +function edit_tab(s::MIState, jump_spaces=false, delete_trailing=jump_spaces) + if tab_should_complete(s) + complete_line(s) + else + edit_insert_tab(buffer(s), jump_spaces, delete_trailing) + refresh_line(s) + :edit_insert_tab + end end -function edit_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_spaces) +function edit_insert_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_spaces) i = position(buf) if jump_spaces && i < buf.size && buf.data[i+1] == _space spaces = findnext(_notspace, buf.data[i+1:buf.size], 1) @@ -1793,29 +1832,28 @@ function prompt!(term, prompt, s = init_state(term, prompt)) kmap = keymap(s, prompt) fcn = match_input(kmap, s) kdata = keymap_data(s, prompt) + local action # errors in keymaps shouldn't cause the REPL to fail, so wrap in a # try/catch block - local state try - state = fcn(s, kdata) + action = fcn(s, kdata) catch e bt = catch_backtrace() warn(e, bt = bt, prefix = "ERROR (in the keymap): ") # try to cleanup and get `s` back to its original state before returning transition(s, :reset) transition(s, old_state) - state = :done + action = :done end - if state === :abort + action != :ignore && (s.last_action = action) + if action === :abort return buffer(s), false, false - elseif state === :done + elseif action === :done return buffer(s), true, false - elseif state === :suspend + elseif action === :suspend if Sys.isunix() return buffer(s), true, true end - else - @assert state === :ok end end finally @@ -1823,4 +1861,5 @@ function prompt!(term, prompt, s = init_state(term, prompt)) end end + end # module From c2cbef268f65729d81e1bd97bde07650a5a1474c Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 20 Aug 2017 19:33:10 +0200 Subject: [PATCH 4/6] REPL: implement a kill ring (fix part of #8447) After a yank ("^Y"), it's possible to "yank-pop" with the "M-y" binding, which replaces the just-yanked text with an older entry from the "kill-ring". --- base/repl/LineEdit.jl | 82 +++++++++++++++--------- doc/src/manual/interacting-with-julia.md | 9 +-- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 97ea953148a88..9045d2cf67cd6 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -37,17 +37,22 @@ end show(io::IO, x::Prompt) = show(io, string("Prompt(\"", prompt_string(x.prompt), "\",...)")) +"Maximum number of entries in the kill ring queue. +Beyond this number, oldest entries are discarded first." +const KILL_RING_MAX = Ref(100) + mutable struct MIState interface::ModalInterface current_mode::TextInterface aborted::Bool mode_state::Dict - kill_buffer::String + kill_ring::Vector{String} + kill_idx::Int previous_key::Vector{Char} key_repeats::Int last_action::Symbol end -MIState(i, c, a, m) = MIState(i, c, a, m, "", Char[], 0, :begin) +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)") @@ -477,7 +482,7 @@ function splice_buffer!(buf::IOBuffer, r::UnitRange{<:Integer}, ins::AbstractStr seek(buf, pos - length(r)) end if first(r) < buf.mark <= last(r) - unmark(buf) + buf.mark = first(r) elseif buf.mark > last(r) && !isempty(r) buf.mark += sizeof(ins) - length(r) end @@ -626,11 +631,40 @@ function edit_delete_next_word(s) end function edit_yank(s::MIState) - edit_insert(buffer(s), s.kill_buffer) + if isempty(s.kill_ring) + beep(terminal(s)) + return :ignore + end + setmark(s) # necessary for edit_yank_pop + edit_insert(buffer(s), s.kill_ring[mod1(s.kill_idx, end)]) refresh_line(s) :edit_yank end +function edit_yank_pop(s::MIState, require_previous_yank=true) + if require_previous_yank && !(s.last_action in [:edit_yank, :edit_yank_pop]) || + isempty(s.kill_ring) + beep(terminal(s)) + :ignore + else + splice_buffer!(buffer(s), region(s), s.kill_ring[mod1(s.kill_idx-=1, end)]) + refresh_line(s) + :edit_yank_pop + end +end + +function push_kill!(s::MIState, killed::String, concat=false) + isempty(killed) && return false + if concat + s.kill_ring[end] *= killed + else + push!(s.kill_ring, killed) + length(s.kill_ring) > KILL_RING_MAX[] && shift!(s.kill_ring) + end + s.kill_idx = endof(s.kill_ring) + true +end + function edit_kill_line(s::MIState) buf = buffer(s) pos = position(buf) @@ -639,40 +673,27 @@ function edit_kill_line(s::MIState) killbuf = killbuf[1:end-1] char_move_left(buf) end - if !isempty(killbuf) - s.kill_buffer = s.key_repeats > 0 ? s.kill_buffer * killbuf : killbuf - splice_buffer!(buf, pos:position(buf)-1) - refresh_line(s) - :edit_kill_line - else - :ignore - end + push_kill!(s, killbuf, s.key_repeats > 0) || return :ignore + splice_buffer!(buf, pos:position(buf)-1) + refresh_line(s) + :edit_kill_line end function edit_copy_region(s::MIState) buf = buffer(s) - if edit_exchange_point_and_mark(buf) # region non-empty - s.kill_buffer = String(buf.data[region(buf)+1]) - if REGION_ANIMATION_DURATION[] > 0.0 - refresh_line(s) - sleep(REGION_ANIMATION_DURATION[]) - end - edit_exchange_point_and_mark(s) # includes refresh_line - :edit_copy_region - else - :ignore + push_kill!(s, String(buf.data[region(buf)+1])) || return :ignore + if REGION_ANIMATION_DURATION[] > 0.0 + edit_exchange_point_and_mark(s) + sleep(REGION_ANIMATION_DURATION[]) + edit_exchange_point_and_mark(s) end + :edit_copy_region end function edit_kill_region(s::MIState) - reg = region(s) - if !isempty(reg) - s.kill_buffer = splice_buffer!(buffer(s), reg) - refresh_line(s) - :edit_kill_region - else - :ignore - end + push_kill!(s, splice_buffer!(buffer(s), region(s))) || return :ignore + refresh_line(s) + :edit_kill_region end function edit_transpose_chars(s::MIState) @@ -1608,6 +1629,7 @@ AnyDict( "^U" => (s,o...)->edit_clear(s), "^K" => (s,o...)->edit_kill_line(s), "^Y" => (s,o...)->edit_yank(s), + "\ey" => (s,o...)->edit_yank_pop(s), "\ew" => (s,o...)->edit_copy_region(s), "\eW" => (s,o...)->edit_kill_region(s), "^A" => (s,o...)->(move_line_start(s); refresh_line(s)), diff --git a/doc/src/manual/interacting-with-julia.md b/doc/src/manual/interacting-with-julia.md index 45b1129881227..caeb91f2e0e2e 100644 --- a/doc/src/manual/interacting-with-julia.md +++ b/doc/src/manual/interacting-with-julia.md @@ -172,10 +172,11 @@ to do so). | meta-Backspace | Delete the previous word | | `meta-d` | Forward delete the next word | | `^W` | Delete previous text up to the nearest whitespace | -| `meta-w` | Copy the current region in the kill buffer | -| `meta-W` | "Kill" the current region, placed the text in the kill buffer | -| `^K` | "Kill" to end of line, placing the text in the kill buffer | -| `^Y` | "Yank" insert the text from the kill buffer | +| `meta-w` | Copy the current region in the kill ring | +| `meta-W` | "Kill" the current region, placing the text in the kill ring | +| `^K` | "Kill" to end of line, placing the text in the kill ring | +| `^Y` | "Yank" insert the text from the kill ring | +| `meta-y` | Replace a previously yanked text with an older entry from the kill ring | | `^T` | Transpose the characters about the cursor | | `meta-u` | Change the next word to uppercase | | `meta-c` | Change the next word to titlecase | From 2d962c1ee62c9bead4b3e4b619cb72893900543d Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 20 Aug 2017 22:18:53 +0200 Subject: [PATCH 5/6] REPL: specify regions with open-close ranges using Pairs All call sites of `splice_buffer!` were manually substracting 1 to the last position of the range, indicating that it's more natural to specify a region with an open-close range. Therefore, `splice_buffer!` and `edit_replace` (thin wrapper of `splice_buffer!`) are merged into `edit_splice!`, which takes a `Region` (a pair of integers) object instead of a range. --- base/precompile.jl | 2 +- base/repl/LineEdit.jl | 103 ++++++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/base/precompile.jl b/base/precompile.jl index 03a87add90607..1f41808501d51 100644 --- a/base/precompile.jl +++ b/base/precompile.jl @@ -492,7 +492,7 @@ precompile(Tuple{typeof(Base.read), Base.TTY, Type{UInt8}}) precompile(Tuple{typeof(Base.throw_boundserror), Array{UInt8, 1}, Tuple{Base.UnitRange{Int64}}}) precompile(Tuple{typeof(Base.deleteat!), Array{UInt8, 1}, Base.UnitRange{Int64}}) precompile(Tuple{typeof(Base.splice!), Array{UInt8, 1}, Base.UnitRange{Int64}, Array{UInt8, 1}}) -precompile(Tuple{typeof(Base.LineEdit.splice_buffer!), Base.GenericIOBuffer{Array{UInt8, 1}}, Base.UnitRange{Int64}, String}) +precompile(Tuple{typeof(Base.LineEdit.edit_splice!), Base.GenericIOBuffer{Array{UInt8, 1}}, Base.Pair{Int,Int}, String}) precompile(Tuple{typeof(Base.LineEdit.refresh_multi_line), Base.Terminals.TerminalBuffer, Base.LineEdit.PromptState}) precompile(Tuple{typeof(Base.LineEdit.edit_insert), Base.GenericIOBuffer{Array{UInt8, 1}}, String}) precompile(Tuple{typeof(Base.LineEdit.edit_insert), Base.LineEdit.PromptState, String}) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 9045d2cf67cd6..8a448187f6fd7 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -7,7 +7,7 @@ using ..Terminals import ..Terminals: raw!, width, height, cmove, getX, getY, clear_line, beep -import Base: ensureroom, peek, show, AnyDict +import Base: ensureroom, peek, show, AnyDict, position abstract type TextInterface end abstract type ModeState end @@ -78,11 +78,14 @@ setmark(s) = mark(buffer(s)) # the default mark is 0 getmark(s) = max(0, buffer(s).mark) -# given two buffer positions delimitating a region -# as an close-open range, return a range of the included -# positions, 0-based (suitable for splice_buffer!) -region(a::Int, b::Int) = ((a, b) = minmax(a, b); a:b-1) -region(s) = region(getmark(s), position(buffer(s))) +const Region = Pair{<:Integer,<:Integer} + +_region(s) = getmark(s) => position(s) +region(s) = Pair(extrema(_region(s))...) + +indexes(reg::Region) = first(reg)+1:last(reg) + +content(s, reg::Region = 0=>buffer(s).size) = String(buffer(s).data[indexes(reg)]) const REGION_ANIMATION_DURATION = Ref(0.2) @@ -92,7 +95,7 @@ input_string_newlines(s::PromptState) = count(c->(c == '\n'), input_string(s)) function input_string_newlines_aftercursor(s::PromptState) str = input_string(s) isempty(str) && return 0 - rest = str[nextind(str, position(s.input_buffer)):end] + rest = str[nextind(str, position(s)):end] return count(c->(c == '\n'), rest) end @@ -182,17 +185,17 @@ function complete_line(s::PromptState, repeats) show_completions(s, completions) elseif length(completions) == 1 # Replace word by completion - prev_pos = position(s.input_buffer) + prev_pos = position(s) seek(s.input_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.input_buffer), prev_pos, completions[1]) + edit_splice!(s, position(s) => 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) + prev_pos = position(s) seek(s.input_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.input_buffer), prev_pos, p) + edit_splice!(s, position(s) => prev_pos, p) elseif repeats > 0 show_completions(s, completions) end @@ -358,7 +361,7 @@ end edit_move_left(s::PromptState) = edit_move_left(s.input_buffer) && refresh_line(s) function edit_move_word_left(s) - if position(s.input_buffer) > 0 + if position(s) > 0 char_move_word_left(s.input_buffer) refresh_line(s) end @@ -429,7 +432,7 @@ function edit_move_up(buf::IOBuffer) npos = rsearch(buf.data, '\n', position(buf)) npos == 0 && return false # we're in the first line # We're interested in character count, not byte count - offset = length(String(buf.data[(npos+1):(position(buf))])) + offset = length(content(buf, npos => position(buf))) npos2 = rsearch(buf.data, '\n', npos-1) seek(buf, npos2) for _ = 1:offset @@ -471,30 +474,31 @@ function edit_move_down(s) changed end -# splice! for IOBuffer: convert from 0-indexed positions, update the size, -# and keep the cursor position stable with the text +# splice! for IOBuffer: convert from close-open region to index, update the size, +# and keep the cursor position and mark stable with the text # returns the removed portion as a String -function splice_buffer!(buf::IOBuffer, r::UnitRange{<:Integer}, ins::AbstractString = "") +function edit_splice!(s, r::Region=region(s), ins::AbstractString = "") + A, B = first(r), last(r) + A >= B && isempty(ins) && return String(ins) + buf = buffer(s) pos = position(buf) - if pos in r - seek(buf, first(r)) - elseif pos > last(r) - seek(buf, pos - length(r)) - end - if first(r) < buf.mark <= last(r) - buf.mark = first(r) - elseif buf.mark > last(r) && !isempty(r) - buf.mark += sizeof(ins) - length(r) - end - ret = splice!(buf.data, r + 1, Vector{UInt8}(ins)) # position(), etc, are 0-indexed - buf.size = buf.size + sizeof(ins) - length(r) + if A <= pos < B + seek(buf, A) + elseif B <= pos + seek(buf, pos - B + A) + end + if A < buf.mark < B + buf.mark = A + elseif A < B <= buf.mark + buf.mark += sizeof(ins) - B + A + end + ret = splice!(buf.data, A+1:B, Vector{UInt8}(ins)) # position(), etc, are 0-indexed + buf.size = buf.size + sizeof(ins) - B + A seek(buf, position(buf) + sizeof(ins)) String(ret) end -function edit_replace(s, from, to, str) - splice_buffer!(buffer(s), from:to-1, str) -end +edit_splice!(s, ins::AbstractString) = edit_splice!(s, region(s), ins) function edit_insert(s::PromptState, c) buf = s.input_buffer @@ -518,7 +522,7 @@ function edit_insert(buf::IOBuffer, c) return write(buf, c) else s = string(c) - splice_buffer!(buf, position(buf):position(buf)-1, s) + edit_splice!(buf, position(buf) => position(buf), s) return sizeof(s) end end @@ -578,7 +582,7 @@ function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=align) end end end - splice_buffer!(buf, newpos:oldpos-1) + edit_splice!(buf, newpos => oldpos) return true end @@ -589,7 +593,7 @@ function edit_delete(buf::IOBuffer) eof(buf) && return false oldpos = position(buf) char_move_right(buf) - splice_buffer!(buf, oldpos:position(buf)-1) + edit_splice!(buf, oldpos => position(buf)) true end @@ -598,9 +602,10 @@ function edit_werase(buf::IOBuffer) char_move_word_left(buf, isspace) pos0 = position(buf) pos0 < pos1 || return false - splice_buffer!(buf, pos0:pos1-1) + edit_splice!(buf, pos0 => pos1) true end + edit_werase(s::MIState) = (edit_werase(buffer(s)) && refresh_line(s); :edit_werase) function edit_delete_prev_word(buf::IOBuffer) @@ -608,9 +613,10 @@ function edit_delete_prev_word(buf::IOBuffer) char_move_word_left(buf) pos0 = position(buf) pos0 < pos1 || return false - splice_buffer!(buf, pos0:pos1-1) + edit_splice!(buf, pos0 => pos1) true end + function edit_delete_prev_word(s::MIState) edit_delete_prev_word(buffer(s)) && refresh_line(s) :edit_delete_prev_word @@ -621,7 +627,7 @@ function edit_delete_next_word(buf::IOBuffer) char_move_word_right(buf) pos1 = position(buf) pos0 < pos1 || return false - splice_buffer!(buf, pos0:pos1-1) + edit_splice!(buf, pos0 => pos1) true end @@ -647,7 +653,7 @@ function edit_yank_pop(s::MIState, require_previous_yank=true) beep(terminal(s)) :ignore else - splice_buffer!(buffer(s), region(s), s.kill_ring[mod1(s.kill_idx-=1, end)]) + edit_splice!(s, s.kill_ring[mod1(s.kill_idx-=1, end)]) refresh_line(s) :edit_yank_pop end @@ -674,14 +680,14 @@ function edit_kill_line(s::MIState) char_move_left(buf) end push_kill!(s, killbuf, s.key_repeats > 0) || return :ignore - splice_buffer!(buf, pos:position(buf)-1) + edit_splice!(buf, pos => position(buf)) refresh_line(s) :edit_kill_line end function edit_copy_region(s::MIState) buf = buffer(s) - push_kill!(s, String(buf.data[region(buf)+1])) || return :ignore + push_kill!(s, content(buf, region(buf))) || return :ignore if REGION_ANIMATION_DURATION[] > 0.0 edit_exchange_point_and_mark(s) sleep(REGION_ANIMATION_DURATION[]) @@ -691,7 +697,7 @@ function edit_copy_region(s::MIState) end function edit_kill_region(s::MIState) - push_kill!(s, splice_buffer!(buffer(s), region(s))) || return :ignore + push_kill!(s, edit_splice!(s)) || return :ignore refresh_line(s) :edit_kill_region end @@ -734,8 +740,8 @@ function edit_transpose_words(buf::IOBuffer, mode=:emacs) char_move_word_right(buf) e1 = position(buf) e1 >= b2 && (seek(buf, pos); return false) - word2 = splice!(buf.data, b2+1:e2, buf.data[b1+1:e1]) - splice!(buf.data, b1+1:e1, word2) + word2 = edit_splice!(buf, b2 => e2, content(buf, b1 => e1)) + edit_splice!(buf, b1 => e1, word2) seek(buf, e2) true end @@ -755,8 +761,7 @@ function edit_replace_word_right(buf::IOBuffer, replace::Function) char_move_word_right(buf) e = position(buf) e == b && return false - newstr = replace(String(buf.data[b+1:e])) - splice_buffer!(buf, b:e-1, newstr) + edit_splice!(buf, b => e, replace(content(buf, b => e))) true end @@ -1323,9 +1328,9 @@ function complete_line(s::SearchState, repeats) completions, partial, should_complete = complete_line(s.histprompt.complete, s) # For now only allow exact completions in search mode if length(completions) == 1 - prev_pos = position(s.query_buffer) + prev_pos = position(s) seek(s.query_buffer, prev_pos-sizeof(partial)) - edit_replace(s, position(s.query_buffer), prev_pos, completions[1]) + edit_splice!(s, position(s) => prev_pos, completions[1]) end end @@ -1563,7 +1568,7 @@ function edit_insert_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_ if jump_spaces && i < buf.size && buf.data[i+1] == _space spaces = findnext(_notspace, buf.data[i+1:buf.size], 1) if delete_trailing && (spaces == 0 || buf.data[i+spaces] == _newline) - splice_buffer!(buf, i:(spaces == 0 ? buf.size-1 : i+spaces-2)) + edit_splice!(buf, i => (spaces == 0 ? buf.size : i+spaces-1)) else jump = spaces == 0 ? buf.size : i+spaces-1 return seek(buf, jump) @@ -1838,6 +1843,8 @@ buffer(s::SearchState) = s.query_buffer buffer(s::PrefixSearchState) = s.response_buffer buffer(s::IOBuffer) = s +position(s::Union{MIState,ModeState}) = position(buffer(s)) + 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) From 8422d505da28db963bb2bd891223391638e1ff9c Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Thu, 24 Aug 2017 17:53:41 +0200 Subject: [PATCH 6/6] REPL: add tests for the kill ring --- test/lineedit.jl | 196 +++++++++++++++++++++++++++-------------------- 1 file changed, 112 insertions(+), 84 deletions(-) diff --git a/test/lineedit.jl b/test/lineedit.jl index 8bbf881d7adb4..cb275d37d4da1 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -1,9 +1,32 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using Base.LineEdit +using Base.LineEdit: edit_insert, buffer, content, setmark, getmark + isdefined(Main, :TestHelpers) || @eval Main include(joinpath(dirname(@__FILE__), "TestHelpers.jl")) using TestHelpers +# no need to have animation in tests +LineEdit.REGION_ANIMATION_DURATION[] = 0.001 + +## helper functions + +function new_state() + term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer()) + LineEdit.init_state(term, ModalInterface([Prompt("test> ")])) +end + +charseek(buf, i) = seek(buf, Base.unsafe_chr2ind(content(buf), i+1)-1) +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)) +end + + function run_test(d,buf) global a_foo, b_foo, a_bar, b_bar a_foo = b_foo = a_bar = b_bar = 0 @@ -277,25 +300,25 @@ seekend(buf) @test LineEdit.edit_delete_prev_word(buf) @test position(buf) == 5 @test buf.size == 5 -@test String(buf.data[1:buf.size]) == "type " +@test content(buf) == "type " buf = IOBuffer("4 +aaa+ x") seek(buf,8) @test LineEdit.edit_delete_prev_word(buf) @test position(buf) == 3 @test buf.size == 4 -@test String(buf.data[1:buf.size]) == "4 +x" +@test content(buf) == "4 +x" buf = IOBuffer("x = func(arg1,arg2 , arg3)") seekend(buf) LineEdit.char_move_word_left(buf) @test position(buf) == 21 @test LineEdit.edit_delete_prev_word(buf) -@test String(buf.data[1:buf.size]) == "x = func(arg1,arg3)" +@test content(buf) == "x = func(arg1,arg3)" @test LineEdit.edit_delete_prev_word(buf) -@test String(buf.data[1:buf.size]) == "x = func(arg3)" +@test content(buf) == "x = func(arg3)" @test LineEdit.edit_delete_prev_word(buf) -@test String(buf.data[1:buf.size]) == "x = arg3)" +@test content(buf) == "x = arg3)" # Unicode combining characters let buf = IOBuffer() @@ -305,7 +328,7 @@ let buf = IOBuffer() LineEdit.edit_move_right(buf) @test nb_available(buf) == 0 LineEdit.edit_backspace(buf) - @test String(buf.data[1:buf.size]) == "a" + @test content(buf) == "a" end ## edit_transpose_chars ## @@ -313,45 +336,41 @@ let buf = IOBuffer() LineEdit.edit_insert(buf, "abcde") seek(buf,0) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "abcde" + @test content(buf) == "abcde" LineEdit.char_move_right(buf) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "bacde" + @test content(buf) == "bacde" LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "bcade" + @test content(buf) == "bcade" seekend(buf) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "bcaed" + @test content(buf) == "bcaed" LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "bcade" + @test content(buf) == "bcade" seek(buf, 0) LineEdit.edit_clear(buf) LineEdit.edit_insert(buf, "αβγδε") seek(buf,0) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "αβγδε" + @test content(buf) == "αβγδε" LineEdit.char_move_right(buf) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "βαγδε" + @test content(buf) == "βαγδε" LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "βγαδε" + @test content(buf) == "βγαδε" seekend(buf) LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "βγαεδ" + @test content(buf) == "βγαεδ" LineEdit.edit_transpose_chars(buf) - @test String(buf.data[1:buf.size]) == "βγαδε" + @test content(buf) == "βγαδε" end @testset "edit_word_transpose" begin buf = IOBuffer() mode = Ref{Symbol}() - function transpose!(i) # i: char indice - seek(buf, Base.unsafe_chr2ind(String(take!(copy(buf))), i+1)-1) - LineEdit.edit_transpose_words(buf, mode[]) - str = String(take!(copy(buf))) - str, Base.unsafe_ind2chr(str, position(buf)+1)-1 - end + transpose!(i) = transform!(buf -> LineEdit.edit_transpose_words(buf, mode[]), + buf, i)[1:2] mode[] = :readline LineEdit.edit_insert(buf, "àbç def gh ") @@ -379,13 +398,11 @@ end @test transpose!(13) == ("àbç gh def", 13) end -let - term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer()) - s = LineEdit.init_state(term, ModalInterface([Prompt("test> ")])) - buf = LineEdit.buffer(s) +let s = new_state() + buf = buffer(s) LineEdit.edit_insert(s,"first line\nsecond line\nthird line") - @test String(buf.data[1:buf.size]) == "first line\nsecond line\nthird line" + @test content(buf) == "first line\nsecond line\nthird line" ## edit_move_line_start/end ## seek(buf, 0) @@ -414,11 +431,11 @@ let s.key_repeats = 1 # Manually flag a repeated keypress LineEdit.edit_kill_line(s) s.key_repeats = 0 - @test String(buf.data[1:buf.size]) == "second line\nthird line" + @test content(buf) == "second line\nthird line" LineEdit.move_line_end(s) LineEdit.edit_move_right(s) LineEdit.edit_yank(s) - @test String(buf.data[1:buf.size]) == "second line\nfirst line\nthird line" + @test content(buf) == "second line\nfirst line\nthird line" end # Issue 7845 @@ -439,16 +456,16 @@ let end @testset "function prompt indentation" begin - term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer(), false) + s = new_state() + term = Base.LineEdit.terminal(s) # default prompt: PromptState.indent should not be set to a final fixed value - s = LineEdit.init_state(term, ModalInterface([Prompt("julia> ")])) ps::LineEdit.PromptState = s.mode_state[s.current_mode] @test ps.indent == -1 # the prompt is modified afterwards to a function ps.p.prompt = let i = 0 () -> ["Julia is Fun! > ", "> "][mod1(i+=1, 2)] # lengths are 16 and 2 end - buf = LineEdit.buffer(ps) + buf = buffer(ps) write(buf, "begin\n julia = :fun\nend") outbuf = IOBuffer() termbuf = Base.Terminals.TerminalBuffer(outbuf) @@ -458,120 +475,107 @@ end "\r\e[16C julia = :fun\n" * "\r\e[16Cend\r\e[19C" LineEdit.refresh_multi_line(termbuf, term, ps) - @test String(take!(copy(outbuf))) == + @test String(take!(outbuf)) == "\r\e[0K\e[1A\r\e[0K\e[1A\r\e[0K\e[1m> \e[0m\r\e[2Cbegin\n" * "\r\e[2C julia = :fun\n" * "\r\e[2Cend\r\e[5C" end @testset "tab/backspace alignment feature" begin - 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 + s = new_state() move_left(s, n) = for x = 1:n LineEdit.edit_move_left(s) end - bufpos(s::Base.LineEdit.MIState) = position(LineEdit.buffer(s)) - LineEdit.edit_insert(s, "for x=1:10\n") LineEdit.edit_tab(s) - @test bufferdata(s) == "for x=1:10\n " + @test content(s) == "for x=1:10\n " LineEdit.edit_backspace(s, true, false) - @test bufferdata(s) == "for x=1:10\n" + @test content(s) == "for x=1:10\n" LineEdit.edit_insert(s, " ") - @test bufpos(s) == 13 + @test position(s) == 13 LineEdit.edit_tab(s) - @test bufferdata(s) == "for x=1:10\n " + @test content(s) == "for x=1:10\n " LineEdit.edit_insert(s, " ") LineEdit.edit_backspace(s, true, false) - @test bufferdata(s) == "for x=1:10\n " + @test content(s) == "for x=1:10\n " LineEdit.edit_insert(s, "éé=3 ") LineEdit.edit_tab(s) - @test bufferdata(s) == "for x=1:10\n éé=3 " + @test content(s) == "for x=1:10\n éé=3 " LineEdit.edit_backspace(s, true, false) - @test bufferdata(s) == "for x=1:10\n éé=3" + @test content(s) == "for x=1:10\n éé=3" LineEdit.edit_insert(s, "\n 1∉x ") LineEdit.edit_tab(s) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " + @test content(s) == "for x=1:10\n éé=3\n 1∉x " LineEdit.edit_backspace(s, false, false) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " + @test content(s) == "for x=1:10\n éé=3\n 1∉x " LineEdit.edit_backspace(s, true, false) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " + @test content(s) == "for x=1:10\n éé=3\n 1∉x " LineEdit.edit_move_word_left(s) LineEdit.edit_tab(s) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " + @test content(s) == "for x=1:10\n éé=3\n 1∉x " LineEdit.move_line_start(s) - @test bufpos(s) == 22 + @test position(s) == 22 LineEdit.edit_tab(s, true) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " - @test bufpos(s) == 30 + @test content(s) == "for x=1:10\n éé=3\n 1∉x " + @test position(s) == 30 LineEdit.edit_move_left(s) - @test bufpos(s) == 29 + @test position(s) == 29 LineEdit.edit_backspace(s, true, true) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " - @test bufpos(s) == 26 + @test content(s) == "for x=1:10\n éé=3\n 1∉x " + @test position(s) == 26 LineEdit.edit_tab(s, false) # same as edit_tab(s, true) here - @test bufpos(s) == 30 + @test position(s) == 30 move_left(s, 6) - @test bufpos(s) == 24 + @test position(s) == 24 LineEdit.edit_backspace(s, true, true) - @test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x " - @test bufpos(s) == 22 + @test content(s) == "for x=1:10\n éé=3\n 1∉x " + @test position(s) == 22 LineEdit.edit_kill_line(s) LineEdit.edit_insert(s, ' '^10) move_left(s, 7) - @test bufferdata(s) == "for x=1:10\n éé=3\n " - @test bufpos(s) == 25 + @test content(s) == "for x=1:10\n éé=3\n " + @test position(s) == 25 LineEdit.edit_tab(s, true, false) - @test bufpos(s) == 32 + @test position(s) == 32 move_left(s, 7) LineEdit.edit_tab(s, true, true) - @test bufpos(s) == 26 - @test bufferdata(s) == "for x=1:10\n éé=3\n " + @test position(s) == 26 + @test content(s) == "for x=1:10\n éé=3\n " # test again the same, when there is a next line LineEdit.edit_insert(s, " \nend") move_left(s, 11) - @test bufpos(s) == 25 + @test position(s) == 25 LineEdit.edit_tab(s, true, false) - @test bufpos(s) == 32 + @test position(s) == 32 move_left(s, 7) LineEdit.edit_tab(s, true, true) - @test bufpos(s) == 26 - @test bufferdata(s) == "for x=1:10\n éé=3\n \nend" + @test position(s) == 26 + @test content(s) == "for x=1:10\n éé=3\n \nend" end @testset "newline alignment feature" begin - 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 - + s = new_state() LineEdit.edit_insert(s, "for x=1:10\n é = 1") LineEdit.edit_insert_newline(s) - @test bufferdata(s) == "for x=1:10\n é = 1\n " + @test content(s) == "for x=1:10\n é = 1\n " LineEdit.edit_insert(s, " b = 2") LineEdit.edit_insert_newline(s) - @test bufferdata(s) == "for x=1:10\n é = 1\n b = 2\n " + @test content(s) == "for x=1:10\n é = 1\n b = 2\n " # after an empty line, should still insert the expected number of spaces LineEdit.edit_insert_newline(s) - @test bufferdata(s) == "for x=1:10\n é = 1\n b = 2\n \n " + @test content(s) == "for x=1:10\n é = 1\n b = 2\n \n " LineEdit.edit_insert_newline(s, 0) - @test bufferdata(s) == "for x=1:10\n é = 1\n b = 2\n \n \n" + @test content(s) == "for x=1:10\n é = 1\n b = 2\n \n \n" LineEdit.edit_insert_newline(s, 2) - @test bufferdata(s) == "for x=1:10\n é = 1\n b = 2\n \n \n\n " + @test content(s) == "for x=1:10\n é = 1\n b = 2\n \n \n\n " # test when point before first letter of the line for i=6:10 LineEdit.edit_clear(s) LineEdit.edit_insert(s, "begin\n x") seek(LineEdit.buffer(s), i) LineEdit.edit_insert_newline(s) - @test bufferdata(s) == "begin\n" * ' '^(i-6) * "\n x" + @test content(s) == "begin\n" * ' '^(i-6) * "\n x" end end @@ -586,3 +590,27 @@ end LineEdit.edit_lower_case(buf) @test String(take!(copy(buf))) == "AA Bb cc" end + +@testset "kill ring" begin + s = new_state() + buf = buffer(s) + edit_insert(s, "ça ≡ nothing") + @test transform!(LineEdit.edit_copy_region, s) == ("ça ≡ nothing", 12, 0) + @test s.kill_ring[end] == "ça ≡ nothing" + @test transform!(LineEdit.edit_exchange_point_and_mark, s)[2:3] == (0, 12) + charseek(buf, 8); setmark(s) + charseek(buf, 1) + @test transform!(LineEdit.edit_kill_region, s) == ("çhing", 1, 1) + @test s.kill_ring[end] == "a ≡ not" + charseek(buf, 0) + @test transform!(LineEdit.edit_yank, s) == ("a ≡ notçhing", 7, 0) + # 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: + @test transform!(LineEdit.edit_yank_pop, s) == ("ça ≡ nothingçhing", 12, 0) + @test s.kill_idx == 1 + LineEdit.edit_kill_line(s) + @test s.kill_ring[end] == "çhing" + @test s.kill_idx == 3 +end