# This file is a part of Julia. License is MIT: https://julialang.org/license module LineEdit using ..Terminals import ..Terminals: raw!, width, height, cmove, getX, getY, clear_line, beep import Base: ensureroom, peek, show, AnyDict abstract type TextInterface end abstract type ModeState end export run_interface, Prompt, ModalInterface, transition, reset_state, edit_insert, keymap struct ModalInterface <: TextInterface modes end mutable struct MIState interface::ModalInterface current_mode aborted::Bool mode_state kill_buffer::String previous_key::Array{Char,1} key_repeats::Int end MIState(i, c, a, m) = MIState(i, c, a, m, "", Char[], 0) function show(io::IO, s::MIState) print(io, "MI State (", s.current_mode, " active)") end mutable struct Prompt <: TextInterface prompt # A string or function to be printed before the prompt. May not change the length of the prompt. # This may be used for changing the color, issuing other terminal escape codes, etc. prompt_prefix # Same as prefix except after the prompt prompt_suffix keymap_dict keymap_func_data complete on_enter on_done hist sticky::Bool end show(io::IO, x::Prompt) = show(io, string("Prompt(\"", x.prompt, "\",...)")) struct InputAreaState num_rows::Int64 curs_row::Int64 end mutable struct PromptState <: ModeState terminal p::Prompt input_buffer::IOBuffer ias::InputAreaState indent::Int end input_string(s::PromptState) = String(s.input_buffer) 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] return count(c->(c == '\n'), rest) end abstract type HistoryProvider end abstract type CompletionProvider end mutable struct EmptyCompletionProvider <: CompletionProvider end mutable struct EmptyHistoryProvider <: HistoryProvider end reset_state(::EmptyHistoryProvider) = nothing complete_line(c::EmptyCompletionProvider, s) = [], true, true terminal(s::IO) = s 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] @eval ($f)(s::MIState, args...) = $(f)(s.mode_state[s.current_mode], args...) end function common_prefix(completions) ret = "" c1 = completions[1] isempty(c1) && return ret i = 1 cc, nexti = next(c1, i) while true for c in completions (i > endof(c) || c[i] != cc) && return ret end ret = string(ret, cc) i >= endof(c1) && return ret i = nexti cc, nexti = next(c1, i) end end # Show available completions function show_completions(s::PromptState, completions) colmax = maximum(map(length, completions)) num_cols = max(div(width(terminal(s)), colmax+2), 1) entries_per_col, r = divrem(length(completions), num_cols) entries_per_col += r != 0 # skip any lines of input after the cursor cmove_down(terminal(s), input_string_newlines_aftercursor(s)) println(terminal(s)) for row = 1:entries_per_col for col = 0:num_cols idx = row + col*entries_per_col if idx <= length(completions) cmove_col(terminal(s), (colmax+2)*col) print(terminal(s), completions[idx]) end end println(terminal(s)) end # make space for the prompt for i = 1:input_string_newlines(s) println(terminal(s)) end end # Prompt Completions complete_line(s::MIState) = complete_line(s.mode_state[s.current_mode], s.key_repeats) function complete_line(s::PromptState, repeats) completions, partial, should_complete = complete_line(s.p.complete, s) if isempty(completions) beep(terminal(s)) elseif !should_complete # should_complete is false for cases where we only want to show # a list of possible completions but not complete, e.g. foo(\t show_completions(s, completions) 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]) 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) elseif repeats > 0 show_completions(s, completions) end end end clear_input_area(terminal, s) = (_clear_input_area(terminal, s.ias); s.ias = InputAreaState(0, 0)) clear_input_area(s) = clear_input_area(s.terminal, s) function _clear_input_area(terminal, state::InputAreaState) # Go to the last line if state.curs_row < state.num_rows cmove_down(terminal, state.num_rows - state.curs_row) end # Clear lines one by one going up for j = 2:state.num_rows clear_line(terminal) cmove_up(terminal) end # Clear top line clear_line(terminal) end prompt_string(s::PromptState) = s.p.prompt prompt_string(s::AbstractString) = s refresh_multi_line(s::ModeState) = refresh_multi_line(terminal(s), s) refresh_multi_line(termbuf::TerminalBuffer, s::ModeState) = refresh_multi_line(termbuf, terminal(s), s) refresh_multi_line(termbuf::TerminalBuffer, term, s::ModeState) = (@assert term == terminal(s); refresh_multi_line(termbuf,s)) function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf, state::InputAreaState, prompt = ""; indent = 0) _clear_input_area(termbuf, state) cols = width(terminal) curs_row = -1 # relative to prompt (1-based) curs_pos = -1 # 1-based column position of the cursor cur_row = 0 # count of the number of rows buf_pos = position(buf) line_pos = buf_pos # Write out the prompt string write_prompt(termbuf, prompt) prompt = prompt_string(prompt) # Count the '\n' at the end of the line if the terminal emulator does (specific to DOS cmd prompt) miscountnl = @static is_windows() ? (isa(Terminals.pipe_reader(terminal), Base.TTY) && !Base.ispty(Terminals.pipe_reader(terminal))) : false lindent = strwidth(prompt) # Now go through the buffer line by line seek(buf, 0) moreinput = true # add a blank line if there is a trailing newline on the last line while moreinput l = readline(buf, chomp=false) moreinput = endswith(l, "\n") # We need to deal with on-screen characters, so use strwidth to compute occupied columns llength = strwidth(l) slength = sizeof(l) cur_row += 1 cmove_col(termbuf, lindent + 1) write(termbuf, l) # We expect to be line after the last valid output line (due to # the '\n' at the end of the previous line) if curs_row == -1 # in this case, we haven't yet written the cursor position line_pos -= slength # '\n' gets an extra pos if line_pos < 0 || !moreinput num_chars = (line_pos >= 0 ? llength : strwidth(l[1:(line_pos + slength)])) curs_row, curs_pos = divrem(lindent + num_chars - 1, cols) curs_row += cur_row curs_pos += 1 # There's an issue if the cursor is after the very right end of the screen. In that case we need to # move the cursor to the next line, and emit a newline if needed if curs_pos == cols # only emit the newline if the cursor is at the end of the line we're writing if line_pos == 0 write(termbuf, "\n") cur_row += 1 end curs_row += 1 curs_pos = 0 cmove_col(termbuf, 1) end end end cur_row += div(max(lindent + llength + miscountnl - 1, 0), cols) lindent = indent end seek(buf, buf_pos) # Let's move the cursor to the right position # The line first n = cur_row - curs_row if n > 0 cmove_up(termbuf, n) end #columns are 1 based cmove_col(termbuf, curs_pos + 1) # Updated cur_row,curs_row return InputAreaState(cur_row, curs_row) end function refresh_multi_line(terminal::UnixTerminal, args...; kwargs...) outbuf = IOBuffer() termbuf = TerminalBuffer(outbuf) ret = refresh_multi_line(termbuf, terminal, args...;kwargs...) # Output the entire refresh at once write(terminal, take!(outbuf)) flush(terminal) return ret end # Edit functionality is_non_word_char(c) = c in " \t\n\"\\'`@\$><=:;|&{}()[].,+-*/?%^~" function reset_key_repeats(f::Function, s::MIState) key_repeats_sav = s.key_repeats try s.key_repeats = 0 f() finally s.key_repeats = key_repeats_sav end end char_move_left(s::PromptState) = char_move_left(s.input_buffer) function char_move_left(buf::IOBuffer) while position(buf) > 0 seek(buf, position(buf)-1) c = peek(buf) (((c & 0x80) == 0) || ((c & 0xc0) == 0xc0)) && break end pos = position(buf) c = read(buf, Char) seek(buf, pos) c end function edit_move_left(buf::IOBuffer) if position(buf) > 0 #move to the next base UTF8 character to the left while true c = char_move_left(buf) if charwidth(c) != 0 || c == '\n' || position(buf) == 0 break end end return true end return false 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 char_move_word_left(s.input_buffer) refresh_line(s) end end char_move_right(s) = char_move_right(buffer(s)) function char_move_right(buf::IOBuffer) !eof(buf) && read(buf, Char) end function char_move_word_right(buf::IOBuffer, is_delimiter=is_non_word_char) while !eof(buf) && is_delimiter(char_move_right(buf)) end while !eof(buf) pos = position(buf) if is_delimiter(char_move_right(buf)) seek(buf, pos) break end end end function char_move_word_left(buf::IOBuffer, is_delimiter=is_non_word_char) while position(buf) > 0 && is_delimiter(char_move_left(buf)) end while position(buf) > 0 pos = position(buf) if is_delimiter(char_move_left(buf)) seek(buf, pos) break end end end char_move_word_right(s) = char_move_word_right(buffer(s)) char_move_word_left(s) = char_move_word_left(buffer(s)) function edit_move_right(buf::IOBuffer) if !eof(buf) # move to the next base UTF8 character to the right while true c = char_move_right(buf) eof(buf) && break pos = position(buf) nextc = read(buf,Char) seek(buf,pos) (charwidth(nextc) != 0 || nextc == '\n') && break end return true end return false end edit_move_right(s::PromptState) = edit_move_right(s.input_buffer) && refresh_line(s) function edit_move_word_right(s) if !eof(s.input_buffer) char_move_word_right(s) refresh_line(s) end end ## Move line up/down # Querying the terminal is expensive, memory access is cheap # so to find the current column, we find the offset for the start # of the line. 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))])) npos2 = rsearch(buf.data, '\n', npos-1) seek(buf, npos2) for _ = 1:offset pos = position(buf) if read(buf, Char) == '\n' seek(buf, pos) break end end return true end function edit_move_up(s) changed = edit_move_up(buffer(s)) changed && refresh_line(s) changed end function edit_move_down(buf::IOBuffer) npos = rsearch(buf.data[1:buf.size], '\n', position(buf)) # We're interested in character count, not byte count offset = length(String(buf.data[(npos+1):(position(buf))])) npos2 = search(buf.data[1:buf.size], '\n', position(buf)+1) if npos2 == 0 #we're in the last line return false end seek(buf, npos2) for _ = 1:offset pos = position(buf) if eof(buf) || read(buf, Char) == '\n' seek(buf, pos) break end end return true end function edit_move_down(s) changed = edit_move_down(buffer(s)) changed && refresh_line(s) changed end # splice! for IOBuffer: convert from 0-indexed positions, update the size, # and keep the cursor position stable with the text function splice_buffer!(buf::IOBuffer, r::UnitRange{<:Integer}, ins::AbstractString = "") pos = position(buf) if !isempty(r) && 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 buf.size = buf.size + sizeof(ins) - length(r) seek(buf, position(buf) + sizeof(ins)) end function edit_replace(s, from, to, str) splice_buffer!(buffer(s), from:to-1, str) end function edit_insert(s::PromptState, c) buf = s.input_buffer function line_size() p = position(buf) seek(buf, rsearch(buf.data, '\n', p)) ls = p - position(buf) seek(buf, p) return ls end str = string(c) edit_insert(buf, str) offset = s.ias.curs_row == 1 ? sizeof(s.p.prompt) : s.indent if !('\n' in str) && eof(buf) && ((line_size() + offset + sizeof(str) - 1) < width(terminal(s))) # Avoid full update when appending characters to the end # and an update of curs_row isn't necessary (conservatively estimated) write(terminal(s), str) else refresh_line(s) end end function edit_insert(buf::IOBuffer, c) if eof(buf) return write(buf, c) else s = string(c) splice_buffer!(buf, position(buf):position(buf)-1, s) return sizeof(s) end end function edit_backspace(s::PromptState) if edit_backspace(s.input_buffer) refresh_line(s) else beep(terminal(s)) end end function edit_backspace(buf::IOBuffer) if position(buf) > 0 oldpos = position(buf) char_move_left(buf) splice_buffer!(buf, position(buf):oldpos-1) return true else return false end end edit_delete(s) = edit_delete(buffer(s)) ? refresh_line(s) : beep(terminal(s)) function edit_delete(buf::IOBuffer) eof(buf) && return false oldpos = position(buf) char_move_right(buf) splice_buffer!(buf, oldpos:position(buf)-1) true end function edit_werase(buf::IOBuffer) pos1 = position(buf) char_move_word_left(buf, isspace) pos0 = position(buf) pos0 < pos1 || return false splice_buffer!(buf, pos0:pos1-1) true end function edit_werase(s) edit_werase(buffer(s)) && refresh_line(s) end function edit_delete_prev_word(buf::IOBuffer) pos1 = position(buf) char_move_word_left(buf) pos0 = position(buf) pos0 < pos1 || return false splice_buffer!(buf, pos0:pos1-1) true end function edit_delete_prev_word(s) edit_delete_prev_word(buffer(s)) && refresh_line(s) end function edit_delete_next_word(buf::IOBuffer) pos0 = position(buf) char_move_word_right(buf) pos1 = position(buf) pos0 < pos1 || return false splice_buffer!(buf, pos0:pos1-1) true end function edit_delete_next_word(s) edit_delete_next_word(buffer(s)) && refresh_line(s) end function edit_yank(s::MIState) edit_insert(buffer(s), s.kill_buffer) refresh_line(s) end function edit_kill_line(s::MIState) buf = buffer(s) pos = position(buf) killbuf = readline(buf, chomp=false) if length(killbuf) > 1 && killbuf[end] == '\n' 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) end edit_transpose(s) = edit_transpose(buffer(s)) && refresh_line(s) function edit_transpose(buf::IOBuffer) position(buf) == 0 && return false eof(buf) && char_move_left(buf) char_move_left(buf) pos = position(buf) a, b = read(buf, Char), read(buf, Char) seek(buf, pos) write(buf, b, a) return true end edit_clear(buf::IOBuffer) = truncate(buf, 0) function edit_clear(s::MIState) edit_clear(buffer(s)) refresh_line(s) end function replace_line(s::PromptState, l::IOBuffer) s.input_buffer = copy(l) end function replace_line(s::PromptState, l) s.input_buffer.ptr = 1 s.input_buffer.size = 0 write(s.input_buffer, l) end history_prev(::EmptyHistoryProvider) = ("", false) history_next(::EmptyHistoryProvider) = ("", false) history_search(::EmptyHistoryProvider, args...) = false add_history(::EmptyHistoryProvider, s) = nothing add_history(s::PromptState) = add_history(mode(s).hist, s) history_next_prefix(s, hist, prefix) = false history_prev_prefix(s, hist, prefix) = false function history_prev(s, hist) l, ok = history_prev(mode(s).hist) if ok replace_line(s, l) move_input_start(s) refresh_line(s) else beep(terminal(s)) end end function history_next(s, hist) l, ok = history_next(mode(s).hist) if ok replace_line(s, l) move_input_end(s) refresh_line(s) else beep(terminal(s)) end end refresh_line(s) = refresh_multi_line(s) refresh_line(s, termbuf) = refresh_multi_line(termbuf, s) default_completion_cb(::IOBuffer) = [] default_enter_cb(_) = true write_prompt(terminal, s::PromptState) = write_prompt(terminal, s.p) function write_prompt(terminal, p::Prompt) prefix = isa(p.prompt_prefix,Function) ? eval(Expr(:call, p.prompt_prefix)) : p.prompt_prefix suffix = isa(p.prompt_suffix,Function) ? eval(Expr(:call, p.prompt_suffix)) : p.prompt_suffix write(terminal, prefix) write(terminal, Base.text_colors[:bold]) write(terminal, p.prompt) write(terminal, Base.text_colors[:normal]) write(terminal, suffix) end write_prompt(terminal, s::String) = write(terminal, s) ### Keymap Support 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.") buf = IOBuffer() i = start(key) while !done(key, i) c, i = next(key, i) if c == '*' write(buf, '\0') elseif c == '^' c, i = next(key, i) write(buf, uppercase(c)-64) elseif c == '\\' c, i = next(key, i) if c == 'C' c, i = next(key, i) @assert c == '-' c, i = next(key, i) write(buf, uppercase(c)-64) elseif c == 'M' c, i = next(key, i) @assert c == '-' c, i = next(key, i) write(buf, '\e') write(buf, c) end else write(buf, c) end end return String(take!(buf)) end function normalize_keys(keymap::Dict) ret = Dict{Any,Any}() for (k,v) in keymap normalized = normalize_key(k) if haskey(ret,normalized) error("""Multiple spellings of a key in a single keymap (\"$k\" conflicts with existing mapping)""") end ret[normalized] = v end return ret end 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) keymap[c] = value break else keymap[c] = Dict{Char,Any}() end keymap = keymap[c] end end # Redirect a key as if `seq` had been the keysequence instead in a lazy fashion. # This is different from the default eager redirect, which only looks at the current and lower # layers of the stack. struct KeyAlias seq::String 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))) 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) 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 # placeholder for the wildcard (see normalize_key("*")) c != '\0' || return keymap_fcn(nothing, "") push!(cs, c) key = haskey(k, c) ? c : '\0' # 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 keymap_fcn(f::Void, c) = (s, p) -> return :ok function keymap_fcn(f::Function, c) return function (s, p) r = eval(Expr(:call,f,s, p, c)) if isa(r, Symbol) return r else return :ok end end end update_key_repeats(s, keystroke) = nothing function update_key_repeats(s::MIState, keystroke) s.key_repeats = s.previous_key == keystroke ? s.key_repeats + 1 : 0 s.previous_key = keystroke return end ## Conflict fixing # Consider a keymap of the form # # { # "**" => f # "ab" => g # } # # Naively this is transformed into a tree as # # { # '*' => { # '*' => f # } # 'a' => { # 'b' => g # } # } # # However, that's not what we want, because now "ac" is # is not defined. We need to fix this up and turn it into # # { # '*' => { # '*' => f # } # 'a' => { # '*' => f # 'b' => g # } # } # # i.e. copy over the appropraite default subdict # # deep merge where target has higher precedence function keymap_merge!(target::Dict, source::Dict) for k in keys(source) if !haskey(target, k) target[k] = source[k] elseif isa(target[k], Dict) keymap_merge!(target[k], source[k]) else # Ignore, target has higher precedence end end end fixup_keymaps!(d, l, s, sk) = nothing function fixup_keymaps!(dict::Dict, level, s, subkeymap) if level > 0 for d in values(dict) fixup_keymaps!(d, level-1, s, subkeymap) end else if haskey(dict, s) if isa(dict[s], Dict) && isa(subkeymap, Dict) keymap_merge!(dict[s], subkeymap) end else dict[s] = deepcopy(subkeymap) end end end function add_specialisations(dict, subdict, level) default_branch = subdict['\0'] 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) fixup_keymaps!(dict, level, s, default_branch[s]) end end end postprocess!(others) = nothing function postprocess!(dict::Dict) # needs to be done first for every branch if haskey(dict, '\0') add_specialisations(dict, dict, 1) end for (k,v) in dict k == '\0' && continue postprocess!(v) end end function getEntry(keymap,key) v = keymap for c in key if !haskey(v,c) return nothing end v = v[c] end return v end # `target` is the total keymap being built up, already being a nested tree of Dicts. # source is the keymap specified by the user (with normalized keys) function keymap_merge(target,source) ret = copy(target) direct_keys = filter((k,v) -> isa(v, Union{Function, KeyAlias, Void}), source) # first direct entries for key in keys(direct_keys) add_nested_key!(ret, key, source[key]; override = true) end # then redirected entries for key in setdiff(keys(source), keys(direct_keys)) # We first resolve redirects in the source value = source[key] visited = Vector{Any}(0) while isa(value, Union{Char,AbstractString}) value = normalize_key(value) if value in visited error("Eager redirection cycle detected for key " * escape_string(key)) end push!(visited,value) if !haskey(source,value) break end value = source[value] end if isa(value, Union{Char,AbstractString}) value = getEntry(ret, value) if value === nothing error("Could not find redirected value " * escape_string(source[key])) end end add_nested_key!(ret, key, value; override = true) end ret end function keymap_unify(keymaps) ret = Dict{Char,Any}() for keymap in keymaps ret = keymap_merge(ret, keymap) end postprocess!(ret) return ret end function validate_keymap(keymap) for key in keys(keymap) visited_keys = Any[key] v = getEntry(keymap,key) while isa(v,KeyAlias) if v.seq in visited_keys error("Alias cycle detected in keymap") end push!(visited_keys,v.seq) v = getEntry(keymap,v.seq) end end end function keymap(keymaps::Array{<:Dict}) # keymaps is a vector of prioritized keymaps, with highest priority first ret = keymap_unify(map(normalize_keys, reverse(keymaps))) validate_keymap(ret) ret end const escape_defaults = merge!( AnyDict(Char(i) => nothing for i=vcat(1:26, 28:31)), # Ignore control characters by default AnyDict( # And ignore other escape sequences by default "\e*" => nothing, "\e[*" => nothing, "\eO*" => nothing, # Also ignore extended escape sequences # TODO: Support ranges of characters "\e[1**" => nothing, "\e[2**" => nothing, "\e[3**" => nothing, "\e[4**" => nothing, "\e[5**" => nothing, "\e[6**" => nothing, # less commonly used VT220 editing keys "\e[2~" => nothing, # insert "\e[3~" => nothing, # delete "\e[5~" => nothing, # page up "\e[6~" => nothing, # page down # These are different spellings of arrow keys, home keys, etc. # and should always do the same as the canonical key sequence "\e[1~" => KeyAlias("\e[H"), # home "\e[4~" => KeyAlias("\e[F"), # end "\e[7~" => KeyAlias("\e[H"), # home "\e[8~" => KeyAlias("\e[F"), # end "\eOA" => KeyAlias("\e[A"), "\eOB" => KeyAlias("\e[B"), "\eOC" => KeyAlias("\e[C"), "\eOD" => KeyAlias("\e[D"), "\eOH" => KeyAlias("\e[H"), "\eOF" => KeyAlias("\e[F"), ), # set mode commands AnyDict("\e[$(c)h" => nothing for c in 1:20), # reset mode commands AnyDict("\e[$(c)l" => nothing for c in 1:20) ) function write_response_buffer(s::PromptState, data) offset = s.input_buffer.ptr ptr = data.response_buffer.ptr seek(data.response_buffer, 0) write(s.input_buffer, readstring(data.response_buffer)) s.input_buffer.ptr = offset + ptr - 2 data.response_buffer.ptr = ptr refresh_line(s) end mutable struct SearchState <: ModeState terminal histprompt #rsearch (true) or ssearch (false) backward::Bool query_buffer::IOBuffer response_buffer::IOBuffer ias::InputAreaState #The prompt whose input will be replaced by the matched history parent SearchState(terminal, histprompt, backward, query_buffer, response_buffer) = new(terminal, histprompt, backward, query_buffer, response_buffer, InputAreaState(0,0)) end terminal(s::SearchState) = s.terminal function update_display_buffer(s::SearchState, data) history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, false) || beep(terminal(s)) refresh_line(s) end function history_next_result(s::MIState, data::SearchState) history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, true) || beep(terminal(s)) refresh_line(data) end function history_set_backward(s::SearchState, backward) s.backward = backward end input_string(s::SearchState) = String(s.query_buffer) function reset_state(s::SearchState) if s.query_buffer.size != 0 s.query_buffer.size = 0 s.query_buffer.ptr = 1 end if s.response_buffer.size != 0 s.response_buffer.size = 0 s.response_buffer.ptr = 1 end reset_state(s.histprompt.hp) end mutable struct HistoryPrompt{T<:HistoryProvider} <: TextInterface hp::T complete keymap_dict::Dict{Char,Any} HistoryPrompt{T}(hp) where T<:HistoryProvider = new(hp, EmptyCompletionProvider()) end HistoryPrompt(hp::T) where T<:HistoryProvider = HistoryPrompt{T}(hp) init_state(terminal, p::HistoryPrompt) = SearchState(terminal, p, true, IOBuffer(), IOBuffer()) mutable struct PrefixSearchState <: ModeState terminal histprompt prefix::String response_buffer::IOBuffer ias::InputAreaState indent::Int # The modal interface state, if present mi #The prompt whose input will be replaced by the matched history parent PrefixSearchState(terminal, histprompt, prefix, response_buffer) = new(terminal, histprompt, prefix, response_buffer, InputAreaState(0,0), 0) end function show(io::IO, s::PrefixSearchState) print(io, "PrefixSearchState ", isdefined(s,:parent) ? string("(", s.parent, " active)") : "(no parent)", " for ", isdefined(s,:mi) ? s.mi : "no MI") end refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, s::Union{PromptState,PrefixSearchState}) = s.ias = refresh_multi_line(termbuf, terminal, buffer(s), s.ias, s, indent = s.indent) input_string(s::PrefixSearchState) = String(s.response_buffer) # a meta-prompt that presents itself as parent_prompt, but which has an independent keymap # for prefix searching mutable struct PrefixHistoryPrompt{T<:HistoryProvider} <: TextInterface hp::T parent_prompt::Prompt complete keymap_dict::Dict{Char,Any} PrefixHistoryPrompt{T}(hp, parent_prompt) where T<:HistoryProvider = new(hp, parent_prompt, EmptyCompletionProvider()) end PrefixHistoryPrompt(hp::T, parent_prompt) where T<:HistoryProvider = PrefixHistoryPrompt{T}(hp, parent_prompt) init_state(terminal, p::PrefixHistoryPrompt) = PrefixSearchState(terminal, p, "", IOBuffer()) write_prompt(terminal, s::PrefixSearchState) = write_prompt(terminal, s.histprompt.parent_prompt) prompt_string(s::PrefixSearchState) = s.histprompt.parent_prompt.prompt terminal(s::PrefixSearchState) = s.terminal function reset_state(s::PrefixSearchState) if s.response_buffer.size != 0 s.response_buffer.size = 0 s.response_buffer.ptr = 1 end reset_state(s.histprompt.hp) end function transition(f::Function, s::PrefixSearchState, mode) if isdefined(s, :mi) transition(s.mi, mode) end s.parent = mode s.histprompt.parent_prompt = mode if isdefined(s, :mi) transition(f, s.mi, s.histprompt) else f() end end replace_line(s::PrefixSearchState, l::IOBuffer) = s.response_buffer = l function replace_line(s::PrefixSearchState, l) s.response_buffer.ptr = 1 s.response_buffer.size = 0 write(s.response_buffer, l) end function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState) buf = IOBuffer() unsafe_write(buf, pointer(s.query_buffer.data), s.query_buffer.ptr-1) write(buf, "': ") offset = buf.ptr ptr = s.response_buffer.ptr seek(s.response_buffer, 0) write(buf, readstring(s.response_buffer)) buf.ptr = offset + ptr - 1 s.response_buffer.ptr = ptr s.ias = refresh_multi_line(termbuf, s.terminal, buf, s.ias, s.backward ? "(reverse-i-search)`" : "(forward-i-search)`") end state(s::MIState, p) = s.mode_state[p] state(s::PromptState, p) = (@assert s.p == p; s) mode(s::MIState) = s.current_mode mode(s::PromptState) = s.p mode(s::SearchState) = @assert false mode(s::PrefixSearchState) = s.histprompt.parent_prompt # Search Mode completions 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) seek(s.query_buffer, prev_pos-sizeof(partial)) edit_replace(s, position(s.query_buffer), prev_pos, completions[1]) end end function accept_result(s, p) parent = state(s, p).parent transition(s, parent) do replace_line(state(s, parent), state(s, p).response_buffer) end end function copybuf!(dst::IOBuffer, src::IOBuffer) n = src.size ensureroom(dst, n) copy!(dst.data, 1, src.data, 1, n) dst.size = src.size dst.ptr = src.ptr end function enter_search(s::MIState, p::HistoryPrompt, backward::Bool) # a bit of hack to help fix #6325 buf = copy(buffer(s)) parent = mode(s) p.hp.last_mode = mode(s) p.hp.last_buffer = buf transition(s, p) do ss = state(s, p) ss.parent = parent ss.backward = backward truncate(ss.query_buffer, 0) copybuf!(ss.response_buffer, buf) end end function enter_prefix_search(s::MIState, p::PrefixHistoryPrompt, backward::Bool) buf = copy(buffer(s)) parent = mode(s) transition(s, p) do pss = state(s, p) pss.parent = parent pss.histprompt.parent_prompt = parent pss.prefix = String(buf.data[1:position(buf)]) copybuf!(pss.response_buffer, buf) pss.indent = state(s, parent).indent pss.mi = s end pss = state(s, p) if backward history_prev_prefix(pss, pss.histprompt.hp, pss.prefix) else history_next_prefix(pss, pss.histprompt.hp, pss.prefix) end end function setup_search_keymap(hp) p = HistoryPrompt(hp) pkeymap = AnyDict( "^R" => (s,data,c)->(history_set_backward(data, true); history_next_result(s, data)), "^S" => (s,data,c)->(history_set_backward(data, false); history_next_result(s, data)), '\r' => (s,o...)->accept_result(s, p), '\n' => '\r', # Limited form of tab completions '\t' => (s,data,c)->(complete_line(s); update_display_buffer(s, data)), "^L" => (s,data,c)->(Terminals.clear(terminal(s)); update_display_buffer(s, data)), # Backspace/^H '\b' => (s,data,c)->(edit_backspace(data.query_buffer) ? update_display_buffer(s, data) : beep(terminal(s))), 127 => KeyAlias('\b'), # Meta Backspace "\e\b" => (s,data,c)->(edit_delete_prev_word(data.query_buffer) ? update_display_buffer(s, data) : beep(terminal(s))), "\e\x7f" => "\e\b", # Word erase to whitespace "^W" => (s,data,c)->(edit_werase(data.query_buffer) ? update_display_buffer(s, data) : beep(terminal(s))), # ^C and ^D "^C" => (s,data,c)->(edit_clear(data.query_buffer); edit_clear(data.response_buffer); update_display_buffer(s, data); reset_state(data.histprompt.hp); transition(s, data.parent)), "^D" => "^C", # Other ways to cancel search mode (it's difficult to bind \e itself) "^G" => "^C", "\e\e" => "^C", "^K" => (s,o...)->transition(s, state(s, p).parent), "^Y" => (s,data,c)->(edit_yank(s); update_display_buffer(s, data)), "^U" => (s,data,c)->(edit_clear(data.query_buffer); edit_clear(data.response_buffer); update_display_buffer(s, data)), # Right Arrow "\e[C" => (s,o...)->(accept_result(s, p); edit_move_right(s)), # Left Arrow "\e[D" => (s,o...)->(accept_result(s, p); edit_move_left(s)), # Up Arrow "\e[A" => (s,o...)->(accept_result(s, p); edit_move_up(s)), # Down Arrow "\e[B" => (s,o...)->(accept_result(s, p); edit_move_down(s)), "^B" => (s,o...)->(accept_result(s, p); edit_move_left(s)), "^F" => (s,o...)->(accept_result(s, p); edit_move_right(s)), # Meta B "\eb" => (s,o...)->(accept_result(s, p); edit_move_word_left(s)), # Meta F "\ef" => (s,o...)->(accept_result(s, p); edit_move_word_right(s)), # Ctrl-Left Arrow "\e[1;5D" => "\eb", # Ctrl-Left Arrow on rxvt "\eOd" => "\eb", # Ctrl-Right Arrow "\e[1;5C" => "\ef", # Ctrl-Right Arrow on rxvt "\eOc" => "\ef", "^A" => (s,o...)->(accept_result(s, p); move_line_start(s); refresh_line(s)), "^E" => (s,o...)->(accept_result(s, p); move_line_end(s); refresh_line(s)), "^Z" => (s,o...)->(return :suspend), # Try to catch all Home/End keys "\e[H" => (s,o...)->(accept_result(s, p); move_input_start(s); refresh_line(s)), "\e[F" => (s,o...)->(accept_result(s, p); move_input_end(s); refresh_line(s)), # Use ^N and ^P to change search directions and iterate through results "^N" => (s,data,c)->(history_set_backward(data, false); history_next_result(s, data)), "^P" => (s,data,c)->(history_set_backward(data, true); history_next_result(s, data)), # Bracketed paste mode "\e[200~" => (s,data,c)-> begin ps = state(s, mode(s)) input = readuntil(ps.terminal, "\e[201~")[1:(end-6)] edit_insert(data.query_buffer, input); update_display_buffer(s, data) end, "*" => (s,data,c)->(edit_insert(data.query_buffer, c); update_display_buffer(s, data)) ) p.keymap_dict = keymap([pkeymap, escape_defaults]) skeymap = AnyDict( "^R" => (s,o...)->(enter_search(s, p, true)), "^S" => (s,o...)->(enter_search(s, p, false)), ) (p, skeymap) end keymap(state, p::Union{HistoryPrompt,PrefixHistoryPrompt}) = p.keymap_dict keymap_data(state, ::Union{HistoryPrompt, PrefixHistoryPrompt}) = state Base.isempty(s::PromptState) = s.input_buffer.size == 0 on_enter(s::PromptState) = s.p.on_enter(s) move_input_start(s) = (seek(buffer(s), 0)) move_input_end(buf::IOBuffer) = seekend(buf) move_input_end(s) = move_input_end(buffer(s)) function move_line_start(s::MIState) buf = buffer(s) curpos = position(buf) curpos == 0 && return if s.key_repeats > 0 move_input_start(s) else seek(buf, rsearch(buf.data, '\n', curpos)) end end function move_line_end(s::MIState) s.key_repeats > 0 ? move_input_end(s) : move_line_end(buffer(s)) end function move_line_end(buf::IOBuffer) eof(buf) && return pos = search(buf.data, '\n', position(buf)+1) if pos == 0 move_input_end(buf) return end seek(buf, pos-1) end function commit_line(s) move_input_end(s) refresh_line(s) println(terminal(s)) add_history(s) state(s, mode(s)).ias = InputAreaState(0, 0) end """ `Base.LineEdit.tabwidth` controls the presumed tab width of code pasted into the REPL. You can modify it by doing `@eval Base.LineEdit tabwidth = 4`, for example. Must satisfy `0 < tabwidth <= 16`. """ global tabwidth = 8 function bracketed_paste(s) ps = state(s, mode(s)) input = readuntil(ps.terminal, "\e[201~")[1:(end-6)] input = replace(input, '\r', '\n') if position(buffer(s)) == 0 indent = Base.indentation(input; tabwidth=tabwidth)[1] input = Base.unindent(input, indent; tabwidth=tabwidth) end return replace(input, '\t', " "^tabwidth) end const default_keymap = AnyDict( # Tab '\t' => (s,o...)->begin buf = buffer(s) # Yes, we are ignoring the possiblity # the we could be in the middle of a multi-byte # sequence, here but that's ok, since any # whitespace we're interested in is only one byte i = position(buf) if i != 0 c = buf.data[i] if c == UInt8('\n') || c == UInt8('\t') || # hack to allow path completion in cmds # after a space, e.g., `cd `, while still # allowing multiple indent levels (c == UInt8(' ') && i > 3 && buf.data[i-1] == UInt8(' ')) edit_insert(s, " "^4) return end end complete_line(s) refresh_line(s) end, # Enter '\r' => (s,o...)->begin if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1) commit_line(s) return :done else edit_insert(s, '\n') end end, '\n' => KeyAlias('\r'), # Backspace/^H '\b' => (s,o...)->edit_backspace(s), 127 => KeyAlias('\b'), # Meta Backspace "\e\b" => (s,o...)->edit_delete_prev_word(s), "\e\x7f" => "\e\b", # ^D "^D" => (s,o...)->begin if buffer(s).size > 0 edit_delete(s) else println(terminal(s)) return :abort end end, "^B" => (s,o...)->edit_move_left(s), "^F" => (s,o...)->edit_move_right(s), # Meta B "\eb" => (s,o...)->edit_move_word_left(s), # Meta F "\ef" => (s,o...)->edit_move_word_right(s), # Ctrl-Left Arrow "\e[1;5D" => "\eb", # Ctrl-Left Arrow on rxvt "\eOd" => "\eb", # Ctrl-Right Arrow "\e[1;5C" => "\ef", # Ctrl-Right Arrow on rxvt "\eOc" => "\ef", # Meta Enter "\e\r" => (s,o...)->(edit_insert(s, '\n')), "\e\n" => "\e\r", # Simply insert it into the buffer by default "*" => (s,data,c)->(edit_insert(s, c)), "^U" => (s,o...)->edit_clear(s), "^K" => (s,o...)->edit_kill_line(s), "^Y" => (s,o...)->edit_yank(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 "\e[H" => (s,o...)->(move_input_start(s); refresh_line(s)), "\e[F" => (s,o...)->(move_input_end(s); refresh_line(s)), "^L" => (s,o...)->(Terminals.clear(terminal(s)); refresh_line(s)), "^W" => (s,o...)->edit_werase(s), # Meta D "\ed" => (s,o...)->edit_delete_next_word(s), "^C" => (s,o...)->begin try # raise the debugger if present ccall(:jl_raise_debugger, Int, ()) end move_input_end(s) refresh_line(s) print(terminal(s), "^C\n\n") transition(s, :reset) refresh_line(s) end, "^Z" => (s,o...)->(return :suspend), # Right Arrow "\e[C" => (s,o...)->edit_move_right(s), # Left Arrow "\e[D" => (s,o...)->edit_move_left(s), # Up Arrow "\e[A" => (s,o...)->edit_move_up(s), # Down Arrow "\e[B" => (s,o...)->edit_move_down(s), # Delete "\e[3~" => (s,o...)->edit_delete(s), # Bracketed Paste Mode "\e[200~" => (s,o...)->begin input = bracketed_paste(s) edit_insert(s, input) end, "^T" => (s,o...)->edit_transpose(s) ) const history_keymap = AnyDict( "^P" => (s,o...)->(history_prev(s, mode(s).hist)), "^N" => (s,o...)->(history_next(s, mode(s).hist)), # Up Arrow "\e[A" => (s,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)), # Down Arrow "\e[B" => (s,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)), # Page Up "\e[5~" => (s,o...)->(history_prev(s, mode(s).hist)), # Page Down "\e[6~" => (s,o...)->(history_next(s, mode(s).hist)) ) const prefix_history_keymap = merge!( AnyDict( # Up Arrow "\e[A" => (s,data,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix), # Down Arrow "\e[B" => (s,data,c)->history_next_prefix(data, data.histprompt.hp, data.prefix), # by default, pass thru to the parent mode "*" => (s,data,c)->begin accept_result(s, data.histprompt); ps = state(s, mode(s)) map = keymap(ps, mode(s)) match_input(map, s, IOBuffer(c))(s, keymap_data(ps, mode(s))) end, # match escape sequences for pass thru "\e*" => "*", "\e[*" => "*", "\eO*" => "*", "\e[1;5*" => "*", # Ctrl-Arrow "\e[200~" => "*" ), # VT220 editing commands AnyDict("\e[$(n)~" => "*" for n in 1:8), # set mode commands AnyDict("\e[$(c)h" => "*" for c in 1:20), # reset mode commands AnyDict("\e[$(c)l" => "*" for c in 1:20) ) function setup_prefix_keymap(hp, parent_prompt) p = PrefixHistoryPrompt(hp, parent_prompt) p.keymap_dict = keymap([prefix_history_keymap]) pkeymap = AnyDict( # Up Arrow "\e[A" => (s,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)), # Down Arrow "\e[B" => (s,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)), ) (p, pkeymap) end function deactivate(p::TextInterface, s::ModeState, termbuf, term::TextTerminal) clear_input_area(termbuf, s) s end function activate(p::TextInterface, s::ModeState, termbuf, term::TextTerminal) s.ias = InputAreaState(0, 0) refresh_line(s, termbuf) end function activate(p::TextInterface, s::MIState, termbuf, term::TextTerminal) @assert p == s.current_mode activate(p, s.mode_state[s.current_mode], termbuf, term) end activate(m::ModalInterface, s::MIState, termbuf, term::TextTerminal) = activate(s.current_mode, s, termbuf, term) commit_changes(t::UnixTerminal, termbuf) = write(t, take!(termbuf.out_stream)) function transition(f::Function, s::MIState, mode) if mode === :abort s.aborted = true return end if mode === :reset reset_state(s) return end if !haskey(s.mode_state,mode) s.mode_state[mode] = init_state(terminal(s), mode) end termbuf = TerminalBuffer(IOBuffer()) t = terminal(s) s.mode_state[s.current_mode] = deactivate(s.current_mode, s.mode_state[s.current_mode], termbuf, t) s.current_mode = mode f() activate(mode, s.mode_state[mode], termbuf, t) commit_changes(t, termbuf) end transition(s::MIState, mode) = transition((args...)->nothing, s, mode) function reset_state(s::PromptState) if s.input_buffer.size != 0 s.input_buffer.size = 0 s.input_buffer.ptr = 1 end s.ias = InputAreaState(0, 0) end function reset_state(s::MIState) for (mode,state) in s.mode_state reset_state(state) end end const default_keymap_dict = keymap([default_keymap, escape_defaults]) function Prompt(prompt; prompt_prefix = "", prompt_suffix = "", keymap_dict = default_keymap_dict, keymap_func_data = nothing, complete = EmptyCompletionProvider(), on_enter = default_enter_cb, on_done = ()->nothing, hist = EmptyHistoryProvider(), sticky = false) Prompt(prompt, prompt_prefix, prompt_suffix, keymap_dict, keymap_func_data, complete, on_enter, on_done, hist, sticky) end run_interface(::Prompt) = nothing init_state(terminal, prompt::Prompt) = PromptState(terminal, prompt, 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}()) for mode in m.modes s.mode_state[mode] = init_state(terminal, mode) end s end function run_interface(terminal, m::ModalInterface) s::MIState = init_state(terminal, m) while !s.aborted buf, ok, suspend = prompt!(terminal, m, s) while suspend @static if is_unix(); ccall(:jl_repl_raise_sigtstp, Cint, ()); end buf, ok, suspend = prompt!(terminal, m, s) end eval(Main, Expr(:body, Expr(:return, Expr(:call, QuoteNode(mode(state(s, s.current_mode)).on_done), QuoteNode(s), QuoteNode(buf), QuoteNode(ok))))) end end buffer(s::PromptState) = s.input_buffer buffer(s::SearchState) = s.query_buffer buffer(s::PrefixSearchState) = s.response_buffer 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) keymap_data(ms::MIState, m::ModalInterface) = keymap_data(ms.mode_state[ms.current_mode], ms.current_mode) function prompt!(term, prompt, s = init_state(term, prompt)) Base.reseteof(term) raw!(term, true) enable_bracketed_paste(term) try activate(prompt, s, term, term) old_state = mode(s) while true kmap = keymap(s, prompt) fcn = match_input(kmap, s) kdata = keymap_data(s, prompt) # errors in keymaps shouldn't cause the REPL to fail, so wrap in a # try/catch block local state try state = 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 end if state === :abort return buffer(s), false, false elseif state === :done return buffer(s), true, false elseif state === :suspend if is_unix() return buffer(s), true, true end else @assert state === :ok end end finally raw!(term, false) && disable_bracketed_paste(term) end end end # module