From 69eadbc28b1ac522e448372efa62db477c7f9698 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 26 Aug 2020 10:38:32 -0500 Subject: [PATCH] Improve inference broadly throughout REPL (#37081) * REPL: move Options to improve inference Moving this earlier in the load sequence allows one to annotate the return type of some methods. * REPL: add interfaces for abstract types and change one field type Since MIState and others have containers with abstract typing, these interfaces fix inference problems broadly. * IO: improve inferrability of write and unsafe_write * REPL: add argument typing and improve implementations Since entire modules are marked `@nospecialize`, we need to declare argument types anywhere we want good inference. This also improves numerous implementations to ensure inferrability. For the completion methods, notice this changes typeof(ret[2]) from UnitRange{Int64} to UnitRange{Int}. Internally, the methods are using Int rather than Int64, so consistently supporting Int64 would require more extensive changes. * REPL: add argtypes to keymap functions * REPL: test inference in LineEdit --- base/io.jl | 2 +- base/iobuffer.jl | 2 +- base/strings/io.jl | 4 +- stdlib/REPL/src/LineEdit.jl | 495 ++++++++++-------- stdlib/REPL/src/REPL.jl | 125 ++--- stdlib/REPL/src/REPLCompletions.jl | 133 +++-- stdlib/REPL/src/TerminalMenus/AbstractMenu.jl | 20 +- stdlib/REPL/src/TerminalMenus/util.jl | 6 +- stdlib/REPL/src/Terminals.jl | 6 +- stdlib/REPL/src/docview.jl | 45 +- stdlib/REPL/src/options.jl | 56 ++ stdlib/REPL/test/lineedit.jl | 93 ++-- stdlib/REPL/test/replcompletions.jl | 18 +- test/iostream.jl | 5 + test/strings/basic.jl | 6 +- 15 files changed, 557 insertions(+), 459 deletions(-) create mode 100644 stdlib/REPL/src/options.jl diff --git a/base/io.jl b/base/io.jl index 421c2d9a60058..4c0cf6d7cf19e 100644 --- a/base/io.jl +++ b/base/io.jl @@ -358,7 +358,7 @@ function pipe_reader end function pipe_writer end write(io::AbstractPipe, byte::UInt8) = write(pipe_writer(io)::IO, byte) -unsafe_write(io::AbstractPipe, p::Ptr{UInt8}, nb::UInt) = unsafe_write(pipe_writer(io)::IO, p, nb) +unsafe_write(io::AbstractPipe, p::Ptr{UInt8}, nb::UInt) = unsafe_write(pipe_writer(io)::IO, p, nb)::Union{Int,UInt} buffer_writes(io::AbstractPipe, args...) = buffer_writes(pipe_writer(io)::IO, args...) flush(io::AbstractPipe) = flush(pipe_writer(io)::IO) diff --git a/base/iobuffer.jl b/base/iobuffer.jl index 3b363b1a0ab67..a1504b4bd4f63 100644 --- a/base/iobuffer.jl +++ b/base/iobuffer.jl @@ -418,7 +418,7 @@ end function unsafe_write(to::GenericIOBuffer, p::Ptr{UInt8}, nb::UInt) ensureroom(to, nb) ptr = (to.append ? to.size+1 : to.ptr) - written = Int(min(nb, length(to.data) - ptr + 1)) + written = Int(min(nb, Int(length(to.data))::Int - ptr + 1)) towrite = written d = to.data while towrite > 0 diff --git a/base/strings/io.jl b/base/strings/io.jl index 3744e9f902164..8db97f5a86cc3 100644 --- a/base/strings/io.jl +++ b/base/strings/io.jl @@ -178,12 +178,12 @@ string(a::Symbol) = String(a) # note: print uses an encoding determined by `io` (defaults to UTF-8), whereas # write uses an encoding determined by `s` (UTF-8 for `String`) print(io::IO, s::AbstractString) = for c in s; print(io, c); end -write(io::IO, s::AbstractString) = (len = 0; for c in s; len += write(io, c); end; len) +write(io::IO, s::AbstractString) = (len = 0; for c in s; len += Int(write(io, c))::Int; end; len) show(io::IO, s::AbstractString) = print_quoted(io, s) # optimized methods to avoid iterating over chars write(io::IO, s::Union{String,SubString{String}}) = - GC.@preserve s unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))) + GC.@preserve s Int(unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))))::Int print(io::IO, s::Union{String,SubString{String}}) = (write(io, s); nothing) ## printing literal quoted string data ## diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index efff96e4c08ce..9f007f328c7f6 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -3,7 +3,7 @@ module LineEdit import ..REPL -using REPL: AbstractREPL +using REPL: AbstractREPL, Options using ..Terminals import ..Terminals: raw!, width, height, cmove, getX, @@ -12,8 +12,8 @@ import ..Terminals: raw!, width, height, cmove, getX, import Base: ensureroom, show, AnyDict, position using Base: something -abstract type TextInterface end -abstract type ModeState end +abstract type TextInterface end # see interface immediately below +abstract type ModeState end # see interface below abstract type HistoryProvider end abstract type CompletionProvider end @@ -21,6 +21,20 @@ export run_interface, Prompt, ModalInterface, transition, reset_state, edit_inse @nospecialize # use only declared type signatures +const StringLike = Union{Char,String,SubString{String}} + +# interface for TextInterface +function Base.getproperty(ti::TextInterface, name::Symbol) + if name === :hp + return getfield(ti, :hp)::HistoryProvider + elseif name === :complete + return getfield(ti, :complete)::CompletionProvider + elseif name === :keymap_dict + return getfield(ti, :keymap_dict)::Dict{Char,Any} + end + return getfield(ti, name) +end + struct ModalInterface <: TextInterface modes::Vector{TextInterface} end @@ -38,7 +52,7 @@ mutable struct Prompt <: TextInterface complete::CompletionProvider on_enter::Function on_done::Function - hist::HistoryProvider + hist::HistoryProvider # TODO?: rename this `hp` (consistency with other TextInterfaces), or is the type-assert useful for mode(s)? sticky::Bool end @@ -49,7 +63,7 @@ mutable struct MIState interface::ModalInterface current_mode::TextInterface aborted::Bool - mode_state::IdDict{Any,Any} + mode_state::IdDict{TextInterface,ModeState} kill_ring::Vector{String} kill_idx::Int previous_key::Vector{Char} @@ -60,6 +74,9 @@ end MIState(i, c, a, m) = MIState(i, c, a, m, String[], 0, Char[], 0, :none, :none) +const BufferLike = Union{MIState,ModeState,IOBuffer} +const State = Union{MIState,ModeState} + function show(io::IO, s::MIState) print(io, "MI State (", mode(s), " active)") end @@ -91,9 +108,9 @@ options(s::PromptState) = if isdefined(s.p, :repl) && isdefined(s.p.repl, :options) # we can't test isa(s.p.repl, LineEditREPL) as LineEditREPL is defined # in the REPL module - s.p.repl.options + s.p.repl.options::Options else - REPL.GlobalOptions + REPL.GlobalOptions::Options end function setmark(s::MIState, guess_region_active::Bool=true) @@ -105,18 +122,18 @@ function setmark(s::MIState, guess_region_active::Bool=true) end # the default mark is 0 -getmark(s) = max(0, buffer(s).mark) +getmark(s::BufferLike) = max(0, buffer(s).mark) const Region = Pair{Int,Int} -_region(s) = getmark(s) => position(s) -region(s) = Pair(extrema(_region(s))...) +_region(s::BufferLike) = getmark(s) => position(s) +region(s::BufferLike) = Pair(extrema(_region(s))...) -bufend(s) = buffer(s).size +bufend(s::BufferLike) = buffer(s).size axes(reg::Region) = first(reg)+1:last(reg) -content(s, reg::Region = 0=>bufend(s)) = String(buffer(s).data[axes(reg)]) +content(s::BufferLike, reg::Region = 0=>bufend(s)) = String(buffer(s).data[axes(reg)]) function activate_region(s::PromptState, state::Symbol) @assert state in (:mark, :shift, :off) @@ -149,7 +166,7 @@ struct EmptyHistoryProvider <: HistoryProvider end reset_state(::EmptyHistoryProvider) = nothing -complete_line(c::EmptyCompletionProvider, s) = [], true, true +complete_line(c::EmptyCompletionProvider, s) = String[], "", true terminal(s::IO) = s terminal(s::PromptState) = s.terminal @@ -162,27 +179,28 @@ function beep(s::PromptState, duration::Real=options(s).beep_duration, use_current::Bool=options(s).beep_use_current) isinteractive() || return # some tests fail on some platforms s.beeping = min(s.beeping + duration, maxduration) - @async begin - trylock(s.refresh_lock) || return - try - orig_prefix = s.p.prompt_prefix - colors = Base.copymutable(colors) - use_current && push!(colors, prompt_string(orig_prefix)) - i = 0 - while s.beeping > 0.0 - prefix = colors[mod1(i+=1, end)] - s.p.prompt_prefix = prefix + let colors = Base.copymutable(colors) + @async begin + trylock(s.refresh_lock) || return + try + orig_prefix = s.p.prompt_prefix + use_current && push!(colors, prompt_string(orig_prefix)) + i = 0 + while s.beeping > 0.0 + prefix = colors[mod1(i+=1, end)] + s.p.prompt_prefix = prefix + refresh_multi_line(s, beeping=true) + sleep(blink) + s.beeping -= blink + end + s.p.prompt_prefix = orig_prefix refresh_multi_line(s, beeping=true) - sleep(blink) - s.beeping -= blink + s.beeping = 0.0 + catch e + Base.showerror(stdout, e, catch_backtrace()) + finally + unlock(s.refresh_lock) end - s.p.prompt_prefix = orig_prefix - refresh_multi_line(s, beeping=true) - s.beeping = 0.0 - catch e - Base.showerror(stdout, e, catch_backtrace()) - finally - unlock(s.refresh_lock) end end nothing @@ -231,7 +249,7 @@ const COMMAND_GROUPS = :copy => [:edit_copy_region], :misc => [:complete_line, :setmark, :edit_undo!, :edit_redo!]) -const COMMAND_GROUP = Dict(command=>group for (group, commands) in COMMAND_GROUPS for command in commands) +const COMMAND_GROUP = Dict{Symbol,Symbol}(command=>group for (group, commands) in COMMAND_GROUPS for command in commands) command_group(command::Symbol) = get(COMMAND_GROUP, command, :nogroup) command_group(command::Function) = command_group(nameof(command)) @@ -273,7 +291,7 @@ end set_action!(s, command::Symbol) = nothing -function common_prefix(completions) +function common_prefix(completions::Vector{String}) ret = "" c1 = completions[1] isempty(c1) && return ret @@ -291,7 +309,7 @@ function common_prefix(completions) end # Show available completions -function show_completions(s::PromptState, completions) +function show_completions(s::PromptState, completions::Vector{String}) colmax = maximum(map(length, completions)) num_cols = max(div(width(terminal(s)), colmax+2), 1) entries_per_col, r = divrem(length(completions), num_cols) @@ -326,8 +344,8 @@ function complete_line(s::MIState) end end -function complete_line(s::PromptState, repeats) - completions, partial, should_complete = complete_line(s.p.complete, s) +function complete_line(s::PromptState, repeats::Int) + completions, partial, should_complete = complete_line(s.p.complete, s)::Tuple{Vector{String},String,Bool} isempty(completions) && return false if !should_complete # should_complete is false for cases where we only want to show @@ -353,9 +371,9 @@ function complete_line(s::PromptState, repeats) return true 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) +clear_input_area(terminal::AbstractTerminal, s::ModeState) = (_clear_input_area(terminal, s.ias); s.ias = InputAreaState(0, 0)) +clear_input_area(s::ModeState) = clear_input_area(s.terminal, s) +function _clear_input_area(terminal::AbstractTerminal, state::InputAreaState) # Go to the last line if state.curs_row < state.num_rows cmove_down(terminal, state.num_rows - state.curs_row) @@ -383,7 +401,7 @@ refresh_multi_line(termbuf::TerminalBuffer, term, s::ModeState; kw...) = (@asser function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf::IOBuffer, state::InputAreaState, prompt = ""; - indent = 0, region_active = false) + indent::Int = 0, region_active::Bool = false) _clear_input_area(termbuf, state) cols = width(terminal) @@ -396,7 +414,7 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf regstart, regstop = region(buf) written = 0 # Write out the prompt string - lindent = write_prompt(termbuf, prompt, hascolor(terminal)) + lindent = write_prompt(termbuf, prompt, hascolor(terminal))::Int # Count the '\n' at the end of the line if the terminal emulator does (specific to DOS cmd prompt) miscountnl = @static Sys.iswindows() ? (isa(Terminals.pipe_reader(terminal), Base.TTY) && !Base.ispty(Terminals.pipe_reader(terminal))) : false @@ -481,7 +499,7 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf return InputAreaState(cur_row, curs_row) end -function highlight_region(lwrite::AbstractString, regstart::Int, regstop::Int, written::Int, slength::Int) +function highlight_region(lwrite::Union{String,SubString{String}}, regstart::Int, regstop::Int, written::Int, slength::Int) if written <= regstop <= written+slength i = thisind(lwrite, regstop-written) lwrite = lwrite[1:i] * Base.disable_text_style[:reverse] * lwrite[nextind(lwrite, i):end] @@ -505,7 +523,7 @@ end # Edit functionality -is_non_word_char(c) = c in """ \t\n\"\\'`@\$><=:;|&{}()[].,+-*/?%^~""" +is_non_word_char(c::Char) = c in """ \t\n\"\\'`@\$><=:;|&{}()[].,+-*/?%^~""" function reset_key_repeats(f::Function, s::MIState) key_repeats_sav = s.key_repeats @@ -559,7 +577,7 @@ end edit_move_left(s::PromptState) = edit_move_left(s.input_buffer) ? refresh_line(s) : false -function edit_move_word_left(s) +function edit_move_word_left(s::PromptState) if position(s) > 0 char_move_word_left(s.input_buffer) return refresh_line(s) @@ -567,12 +585,12 @@ function edit_move_word_left(s) return nothing end -char_move_right(s) = char_move_right(buffer(s)) +char_move_right(s::MIState) = char_move_right(buffer(s)) function char_move_right(buf::IOBuffer) return !eof(buf) && read(buf, Char) end -function char_move_word_right(buf::IOBuffer, is_delimiter=is_non_word_char) +function char_move_word_right(buf::IOBuffer, is_delimiter::Function=is_non_word_char) while !eof(buf) && is_delimiter(char_move_right(buf)) end while !eof(buf) @@ -584,7 +602,7 @@ function char_move_word_right(buf::IOBuffer, is_delimiter=is_non_word_char) end end -function char_move_word_left(buf::IOBuffer, is_delimiter=is_non_word_char) +function char_move_word_left(buf::IOBuffer, is_delimiter::Function=is_non_word_char) while position(buf) > 0 && is_delimiter(char_move_left(buf)) end while position(buf) > 0 @@ -596,8 +614,8 @@ function char_move_word_left(buf::IOBuffer, is_delimiter=is_non_word_char) end end -char_move_word_right(s) = char_move_word_right(buffer(s)) -char_move_word_left(s) = char_move_word_left(buffer(s)) +char_move_word_right(s::Union{MIState,ModeState}) = char_move_word_right(buffer(s)) +char_move_word_left(s::Union{MIState,ModeState}) = char_move_word_left(buffer(s)) function edit_move_right(buf::IOBuffer) if !eof(buf) @@ -616,7 +634,7 @@ function edit_move_right(buf::IOBuffer) end edit_move_right(s::PromptState) = edit_move_right(s.input_buffer) ? refresh_line(s) : false -function edit_move_word_right(s) +function edit_move_word_right(s::PromptState) if !eof(s.input_buffer) char_move_word_right(s) return refresh_line(s) @@ -645,7 +663,7 @@ function edit_move_up(buf::IOBuffer) end return true end -function edit_move_up(s) +function edit_move_up(s::MIState) set_action!(s, :edit_move_up) changed = edit_move_up(buffer(s)) changed && refresh_line(s) @@ -670,7 +688,7 @@ function edit_move_down(buf::IOBuffer) end return true end -function edit_move_down(s) +function edit_move_down(s::MIState) set_action!(s, :edit_move_down) changed = edit_move_down(buffer(s)) changed && refresh_line(s) @@ -687,7 +705,7 @@ end # 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 edit_splice!(s, r::Region=region(s), ins::AbstractString = ""; rigid_mark::Bool=true) +function edit_splice!(s::BufferLike, r::Region=region(s), ins::String = ""; rigid_mark::Bool=true) A, B = first(r), last(r) A >= B && isempty(ins) && return String(ins) buf = buffer(s) @@ -713,9 +731,9 @@ function edit_splice!(s, r::Region=region(s), ins::AbstractString = ""; rigid_ma return String(ret) end -edit_splice!(s, ins::AbstractString) = edit_splice!(s, region(s), ins) +edit_splice!(s::MIState, ins::AbstractString) = edit_splice!(s, region(s), ins) -function edit_insert(s::PromptState, c) +function edit_insert(s::PromptState, c::StringLike) push_undo(s) buf = s.input_buffer @@ -742,7 +760,7 @@ function edit_insert(s::PromptState, c) str = string(c) edit_insert(buf, str) offset = s.ias.curs_row == 1 || s.indent < 0 ? - sizeof(prompt_string(s.p.prompt)) : s.indent + sizeof(prompt_string(s.p.prompt)::String) : s.indent if !('\n' in str) && eof(buf) && ((position(buf) - beginofline(buf) + # size of current line offset + sizeof(str) - 1) < width(terminal(s))) @@ -754,7 +772,7 @@ function edit_insert(s::PromptState, c) end end -function edit_insert(buf::IOBuffer, c) +function edit_insert(buf::IOBuffer, c::StringLike) if eof(buf) return write(buf, c) else @@ -792,7 +810,7 @@ end # adjust: also delete spaces on the right of the cursor to try to keep aligned what is # on the right function edit_backspace(s::PromptState, align::Bool=options(s).backspace_align, - adjust=options(s).backspace_adjust) + adjust::Bool=options(s).backspace_adjust) push_undo(s) if edit_backspace(buffer(s), align, adjust) return refresh_line(s) @@ -807,9 +825,9 @@ const _space = UInt8(' ') _notspace(c) = c != _space -beginofline(buf, pos=position(buf)) = something(findprev(isequal(_newline), buf.data, pos), 0) +beginofline(buf::IOBuffer, pos::Int=position(buf)) = something(findprev(isequal(_newline), buf.data, pos), 0) -function endofline(buf, pos=position(buf)) +function endofline(buf::IOBuffer, pos::Int=position(buf)) eol = findnext(isequal(_newline), buf.data[pos+1:buf.size], 1) eol === nothing ? buf.size : pos + eol - 1 end @@ -841,7 +859,7 @@ function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=false) return true end -function edit_delete(s) +function edit_delete(s::MIState) set_action!(s, :edit_delete) push_undo(s) if edit_delete(buffer(s)) @@ -903,7 +921,7 @@ function edit_delete_next_word(buf::IOBuffer) return edit_splice!(buf, pos0 => pos1) end -function edit_delete_next_word(s) +function edit_delete_next_word(s::MIState) set_action!(s, :edit_delete_next_word) push_undo(s) if push_kill!(s, edit_delete_next_word(buffer(s))) @@ -926,7 +944,7 @@ function edit_yank(s::MIState) return refresh_line(s) end -function edit_yank_pop(s::MIState, require_previous_yank=true) +function edit_yank_pop(s::MIState, require_previous_yank::Bool=true) set_action!(s, :edit_yank_pop) repeat = s.last_action ∈ (:edit_yank, :edit_yank_pop) if require_previous_yank && !repeat || isempty(s.kill_ring) @@ -940,7 +958,7 @@ function edit_yank_pop(s::MIState, require_previous_yank=true) end end -function push_kill!(s::MIState, killed::String, concat = s.key_repeats > 0; rev=false) +function push_kill!(s::MIState, killed::String, concat::Bool = s.key_repeats > 0; rev::Bool=false) isempty(killed) && return false if concat && !isempty(s.kill_ring) s.kill_ring[end] = rev ? @@ -977,8 +995,8 @@ function edit_kill_line(s::MIState, backwards::Bool=false) end end -edit_kill_line_forwards(s) = edit_kill_line(s, false) -edit_kill_line_backwards(s) = edit_kill_line(s, true) +edit_kill_line_forwards(s::MIState) = edit_kill_line(s, false) +edit_kill_line_backwards(s::MIState) = edit_kill_line(s, true) function edit_copy_region(s::MIState) set_action!(s, :edit_copy_region) @@ -1020,13 +1038,13 @@ function edit_transpose_chars(buf::IOBuffer) return true end -function edit_transpose_words(s) +function edit_transpose_words(s::MIState) set_action!(s, :edit_transpose_words) push_undo(s) return edit_transpose_words(buffer(s)) ? refresh_line(s) : pop_undo(s) end -function edit_transpose_words(buf::IOBuffer, mode=:emacs) +function edit_transpose_words(buf::IOBuffer, mode::Symbol=:emacs) mode in [:readline, :emacs] || throw(ArgumentError("`mode` must be `:readline` or `:emacs`")) pos = position(buf) @@ -1078,7 +1096,7 @@ function edit_transpose_lines_down!(buf::IOBuffer, reg::Region) end # return the region if active, or the current position as a Region otherwise -region_if_active(s)::Region = is_region_active(s) ? region(s) : position(s)=>position(s) +region_if_active(s::MIState)::Region = is_region_active(s) ? region(s) : position(s)=>position(s) function edit_transpose_lines_up!(s::MIState) set_action!(s, :edit_transpose_lines_up!) @@ -1099,20 +1117,20 @@ function edit_transpose_lines_down!(s::MIState) end end -function edit_upper_case(s) +function edit_upper_case(s::BufferLike) set_action!(s, :edit_upper_case) return edit_replace_word_right(s, uppercase) end -function edit_lower_case(s) +function edit_lower_case(s::BufferLike) set_action!(s, :edit_lower_case) return edit_replace_word_right(s, lowercase) end -function edit_title_case(s) +function edit_title_case(s::BufferLike) set_action!(s, :edit_title_case) return edit_replace_word_right(s, titlecase) end -function edit_replace_word_right(s, replace::Function) +function edit_replace_word_right(s::Union{MIState,ModeState}, replace::Function) push_undo(s) return edit_replace_word_right(buffer(s), replace) ? refresh_line(s) : pop_undo(s) end @@ -1148,7 +1166,7 @@ function replace_line(s::PromptState, l::IOBuffer) nothing end -function replace_line(s::PromptState, l, keep_undo=false) +function replace_line(s::PromptState, l::Union{String,SubString{String}}, keep_undo::Bool=false) keep_undo || empty_undo(s) s.input_buffer.ptr = 1 s.input_buffer.size = 0 @@ -1174,7 +1192,7 @@ end # return the indices in buffer(s) of the beginning of each lines # having a non-empty intersection with region(s) -function get_lines_in_region(s)::Vector{Int} +function get_lines_in_region(s::BufferLike) buf = buffer(s) b, e = region(buf) bol = Int[beginofline(buf, b)] # begin of lines @@ -1189,7 +1207,7 @@ end # compute the number of spaces from b till the next non-space on the right # (which can also be "end of line" or "end of buffer") -function leadingspaces(buf::IOBuffer, b::Int)::Int +function leadingspaces(buf::IOBuffer, b::Int) ls = something(findnext(_notspace, buf.data, b+1), 0)-1 ls == -1 && (ls = buf.size) ls -= b @@ -1198,7 +1216,7 @@ end # indent by abs(num) characters, on the right if num >= 0, on the left otherwise # if multiline is true, indent all the lines in the region as a block. -function edit_indent(buf::IOBuffer, num::Int, multiline::Bool)::Bool +function edit_indent(buf::IOBuffer, num::Int, multiline::Bool) bol = multiline ? get_lines_in_region(buf) : Int[beginofline(buf)] if num < 0 # count leading spaces on the lines, which are an upper bound @@ -1231,7 +1249,7 @@ 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) +function history_prev(s::ModeState, hist) l, ok = history_prev(mode(s).hist) if ok replace_line(s, l) @@ -1242,7 +1260,7 @@ function history_prev(s, hist) end nothing end -function history_next(s, hist) +function history_next(s::ModeState, hist) l, ok = history_next(mode(s).hist) if ok replace_line(s, l) @@ -1254,15 +1272,15 @@ function history_next(s, hist) nothing end -refresh_line(s) = refresh_multi_line(s) -refresh_line(s, termbuf) = refresh_multi_line(termbuf, s) +refresh_line(s::BufferLike) = refresh_multi_line(s) +refresh_line(s::BufferLike, termbuf::AbstractTerminal) = refresh_multi_line(termbuf, s) default_completion_cb(::IOBuffer) = [] default_enter_cb(_) = true -write_prompt(terminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color) +write_prompt(terminal::AbstractTerminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color) -function write_prompt(terminal, p::Prompt, color::Bool) +function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool) prefix = prompt_string(p.prompt_prefix) suffix = prompt_string(p.prompt_suffix) write(terminal, prefix) @@ -1306,7 +1324,7 @@ end # returns the width of the written prompt function write_prompt(terminal, s::Union{AbstractString,Function}, color::Bool) @static Sys.iswindows() && _reset_console_mode() - promptstr = prompt_string(s) + promptstr = prompt_string(s)::String write(terminal, promptstr) return textwidth(promptstr) end @@ -1317,7 +1335,7 @@ const wildcard = '\U10f7ff' # "Private Use" Char normalize_key(key::AbstractChar) = string(key) normalize_key(key::Union{Int,UInt8}) = normalize_key(Char(key)) -function normalize_key(key::AbstractString) +function normalize_key(key::Union{String,SubString{String}}) wildcard in key && error("Matching '\U10f7ff' not supported.") buf = IOBuffer() i = firstindex(key) @@ -1389,10 +1407,10 @@ struct KeyAlias KeyAlias(seq) = new(normalize_key(seq)) end -function match_input(f::Function, s, term, cs, keymap) +function match_input(f::Function, s::Union{Nothing,MIState}, term, cs::Vector{Char}, keymap) update_key_repeats(s, cs) c = String(cs) - return function (s, p) + return function (s, p) # s::Union{Nothing,MIState}; p can be (at least) a LineEditREPL, PrefixSearchState, Nothing r = Base.invokelatest(f, s, p, c) if isa(r, Symbol) return r @@ -1403,10 +1421,10 @@ function match_input(f::Function, s, term, cs, keymap) end match_input(k::Nothing, s, term, cs, keymap) = (s,p) -> return :ok -match_input(k::KeyAlias, s, term, cs, keymap) = +match_input(k::KeyAlias, s::Union{Nothing,MIState}, term, cs, keymap::Dict{Char}) = match_input(keymap, s, IOBuffer(k.seq), Char[], keymap) -function match_input(k::Dict, s, term=terminal(s), cs=Char[], keymap = k) +function match_input(k::Dict{Char}, s::Union{Nothing,MIState}, term::Union{AbstractTerminal,IOBuffer}=terminal(s), cs::Vector{Char}=Char[], keymap::Dict{Char} = k) # if we run out of characters to match before resolving an action, # return an empty keymap function eof(term) && return (s, p) -> :abort @@ -1421,7 +1439,7 @@ function match_input(k::Dict, s, term=terminal(s), cs=Char[], keymap = k) end update_key_repeats(s, keystroke) = nothing -function update_key_repeats(s::MIState, keystroke) +function update_key_repeats(s::MIState, keystroke::Vector{Char}) s.key_repeats = s.previous_key == keystroke ? s.key_repeats + 1 : 0 s.previous_key = keystroke return @@ -1660,21 +1678,21 @@ init_state(terminal, p::HistoryPrompt) = SearchState(terminal, p, true, IOBuffer terminal(s::SearchState) = s.terminal -function update_display_buffer(s::SearchState, data) +function update_display_buffer(s::SearchState, data::ModeState) s.failed = !history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, false) s.failed && beep(s) refresh_line(s) nothing end -function history_next_result(s::MIState, data::SearchState) +function history_next_result(s::MIState, data::ModeState) data.failed = !history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, true) data.failed && beep(s) refresh_line(data) nothing end -function history_set_backward(s::SearchState, backward) +function history_set_backward(s::SearchState, backward::Bool) s.backward = backward nothing end @@ -1721,6 +1739,34 @@ mutable struct PrefixSearchState <: ModeState new(terminal, histprompt, prefix, response_buffer, InputAreaState(0,0), 0) end +# interface for ModeState +function Base.getproperty(s::ModeState, name::Symbol) + if name === :terminal + return getfield(s, :terminal)::AbstractTerminal + elseif name === :prompt + return getfield(s, :prompt)::Prompt + elseif name === :histprompt + return getfield(s, :histprompt)::Union{HistoryPrompt,PrefixHistoryPrompt} + elseif name === :parent + return getfield(s, :parent)::Prompt + elseif name === :response_buffer + return getfield(s, :response_buffer)::IOBuffer + elseif name === :ias + return getfield(s, :ias)::InputAreaState + elseif name === :indent + return getfield(s, :indent)::Int + # # unique fields, but no harm in declaring them + # elseif name === :input_buffer + # return getfield(s, :input_buffer)::IOBuffer + # elseif name === :region_active + # return getfield(s, :region_active)::Symbol + # elseif name === :undo_buffers + # return getfield(s, :undo_buffers)::Vector{IOBuffer} + # elseif name === :undo_idx + end + return getfield(s, name) +end + init_state(terminal, p::PrefixHistoryPrompt) = PrefixSearchState(terminal, p, "", IOBuffer()) function show(io::IO, s::PrefixSearchState) @@ -1730,9 +1776,9 @@ function show(io::IO, s::PrefixSearchState) end function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, - s::Union{PromptState,PrefixSearchState}; beeping=false) + s::Union{PromptState,PrefixSearchState}; beeping::Bool=false) beeping || cancel_beep(s) - ias = refresh_multi_line(termbuf, terminal, buffer(s), s.ias, s, + ias = refresh_multi_line(termbuf, terminal, buffer(s), s.ias, s; indent = s.indent, region_active = is_region_active(s)) s.ias = ias @@ -1755,7 +1801,7 @@ function reset_state(s::PrefixSearchState) nothing end -function transition(f::Function, s::PrefixSearchState, mode) +function transition(f::Function, s::PrefixSearchState, mode::Prompt) if isdefined(s, :mi) transition(s.mi, mode) end @@ -1770,7 +1816,7 @@ function transition(f::Function, s::PrefixSearchState, mode) end replace_line(s::PrefixSearchState, l::IOBuffer) = (s.response_buffer = l; nothing) -function replace_line(s::PrefixSearchState, l) +function replace_line(s::PrefixSearchState, l::Union{String,SubString{String}}) s.response_buffer.ptr = 1 s.response_buffer.size = 0 write(s.response_buffer, l) @@ -1794,12 +1840,13 @@ function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState) return ias end -state(s::MIState, p=mode(s)) = s.mode_state[p] -state(s::PromptState, p=mode(s)) = (@assert s.p == p; s) -mode(s::MIState) = s.current_mode -mode(s::PromptState) = s.p +state(s::MIState, p::TextInterface=mode(s)) = s.mode_state[p] +state(s::PromptState, p::Prompt=mode(s)) = (@assert s.p == p; s) + +mode(s::MIState) = s.current_mode # ::TextInterface, and might be a Prompt +mode(s::PromptState) = s.p # ::Prompt mode(s::SearchState) = @assert false -mode(s::PrefixSearchState) = s.histprompt.parent_prompt +mode(s::PrefixSearchState) = s.histprompt.parent_prompt # ::Prompt # Search Mode completions function complete_line(s::SearchState, repeats) @@ -1815,7 +1862,7 @@ function complete_line(s::SearchState, repeats) end accept_result_newmode(hp::HistoryProvider) = nothing -function accept_result(s, p) # p must be either a HistoryPrompt or PrefixHistoryPrompt, probably +function accept_result(s::MIState, p::TextInterface) parent = something(accept_result_newmode(p.hp), state(s, p).parent) transition(s, parent) do replace_line(state(s, parent), state(s, p).response_buffer) @@ -1876,27 +1923,27 @@ 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), + "^R" => (s::MIState,data::ModeState,c)->(history_set_backward(data, true); history_next_result(s, data)), + "^S" => (s::MIState,data::ModeState,c)->(history_set_backward(data, false); history_next_result(s, data)), + '\r' => (s::MIState,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)), + '\t' => (s::MIState,data::ModeState,c)->(complete_line(s); update_display_buffer(s, data)), + "^L" => (s::MIState,data::ModeState,c)->(Terminals.clear(terminal(s)); update_display_buffer(s, data)), # Backspace/^H - '\b' => (s,data,c)->(edit_backspace(data.query_buffer) ? + '\b' => (s::MIState,data::ModeState,c)->(edit_backspace(data.query_buffer) ? update_display_buffer(s, data) : beep(s)), 127 => KeyAlias('\b'), # Meta Backspace - "\e\b" => (s,data,c)->(isempty(edit_delete_prev_word(data.query_buffer)) ? + "\e\b" => (s::MIState,data::ModeState,c)->(isempty(edit_delete_prev_word(data.query_buffer)) ? beep(s) : update_display_buffer(s, data)), "\e\x7f" => "\e\b", # Word erase to whitespace - "^W" => (s,data,c)->(isempty(edit_werase(data.query_buffer)) ? + "^W" => (s::MIState,data::ModeState,c)->(isempty(edit_werase(data.query_buffer)) ? beep(s) : update_display_buffer(s, data)), # ^C and ^D - "^C" => (s,data,c)->(edit_clear(data.query_buffer); + "^C" => (s::MIState,data::ModeState,c)->(edit_clear(data.query_buffer); edit_clear(data.response_buffer); update_display_buffer(s, data); reset_state(data.histprompt.hp); @@ -1905,25 +1952,25 @@ function setup_search_keymap(hp) # 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); + "^K" => (s::MIState,o...)->transition(s, state(s, p).parent), + "^Y" => (s::MIState,data::ModeState,c)->(edit_yank(s); update_display_buffer(s, data)), + "^U" => (s::MIState,data::ModeState,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)), + "\e[C" => (s::MIState,o...)->(accept_result(s, p); edit_move_right(s)), # Left Arrow - "\e[D" => (s,o...)->(accept_result(s, p); edit_move_left(s)), + "\e[D" => (s::MIState,o...)->(accept_result(s, p); edit_move_left(s)), # Up Arrow - "\e[A" => (s,o...)->(accept_result(s, p); edit_move_up(s)), + "\e[A" => (s::MIState,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)), + "\e[B" => (s::MIState,o...)->(accept_result(s, p); edit_move_down(s)), + "^B" => (s::MIState,o...)->(accept_result(s, p); edit_move_left(s)), + "^F" => (s::MIState,o...)->(accept_result(s, p); edit_move_right(s)), # Meta B - "\eb" => (s,o...)->(accept_result(s, p); edit_move_word_left(s)), + "\eb" => (s::MIState,o...)->(accept_result(s, p); edit_move_word_left(s)), # Meta F - "\ef" => (s,o...)->(accept_result(s, p); edit_move_word_right(s)), + "\ef" => (s::MIState,o...)->(accept_result(s, p); edit_move_word_right(s)), # Ctrl-Left Arrow "\e[1;5D" => "\eb", # Ctrl-Left Arrow on rxvt @@ -1932,27 +1979,27 @@ function setup_search_keymap(hp) "\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), + "^A" => (s::MIState,o...)->(accept_result(s, p); move_line_start(s); refresh_line(s)), + "^E" => (s::MIState,o...)->(accept_result(s, p); move_line_end(s); refresh_line(s)), + "^Z" => (s::MIState,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)), + "\e[H" => (s::MIState,o...)->(accept_result(s, p); move_input_start(s); refresh_line(s)), + "\e[F" => (s::MIState,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)), + "^N" => (s::MIState,data::ModeState,c)->(history_set_backward(data, false); history_next_result(s, data)), + "^P" => (s::MIState,data::ModeState,c)->(history_set_backward(data, true); history_next_result(s, data)), # Bracketed paste mode - "\e[200~" => (s,data,c)-> begin + "\e[200~" => (s::MIState,data::ModeState,c)-> begin ps = state(s, mode(s)) input = readuntil(ps.terminal, "\e[201~", keep=false) 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)) + "*" => (s::MIState,data::ModeState,c::StringLike)->(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)), + "^R" => (s::MIState,o...)->(enter_search(s, p, true)), + "^S" => (s::MIState,o...)->(enter_search(s, p, false)), ) return (p, skeymap) end @@ -1964,9 +2011,9 @@ 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); nothing) +move_input_start(s::BufferLike) = (seek(buffer(s), 0); nothing) move_input_end(buf::IOBuffer) = (seekend(buf); nothing) -move_input_end(s) = (move_input_end(buffer(s)); nothing) +move_input_end(s::Union{MIState,ModeState}) = (move_input_end(buffer(s)); nothing) function move_line_start(s::MIState) set_action!(s, :move_line_start) @@ -2019,7 +2066,7 @@ function get_last_word(buf::IOBuffer) word end -function commit_line(s) +function commit_line(s::MIState) cancel_beep(s) move_input_end(s) refresh_line(s) @@ -2030,7 +2077,7 @@ function commit_line(s) nothing end -function bracketed_paste(s; tabwidth::Int=options(s).tabwidth::Int) +function bracketed_paste(s::MIState; tabwidth::Int=options(s).tabwidth) options(s).auto_indent_bracketed_paste = true ps = state(s, mode(s))::PromptState input = readuntil(ps.terminal, "\e[201~") @@ -2042,7 +2089,7 @@ function bracketed_paste(s; tabwidth::Int=options(s).tabwidth::Int) return replace(input, '\t' => " "^tabwidth) end -function tab_should_complete(s) +function tab_should_complete(s::MIState) # Yes, we are ignoring the possibility # the we could be in the middle of a multi-byte # sequence, here but that's ok, since any @@ -2060,7 +2107,7 @@ 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::MIState, jump_spaces=false, delete_trailing=jump_spaces) +function edit_tab(s::MIState, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces) tab_should_complete(s) && return complete_line(s) set_action!(s, :edit_insert_tab) push_undo(s) @@ -2070,7 +2117,7 @@ end # return true iff the content of the buffer is modified # return false when only the position changed -function edit_insert_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_spaces) +function edit_insert_tab(buf::IOBuffer, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces) i = position(buf) if jump_spaces && i < buf.size && buf.data[i+1] == _space spaces = something(findnext(_notspace, buf.data[i+1:buf.size], 1), 0) @@ -2088,7 +2135,7 @@ function edit_insert_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_ return true end -function edit_abort(s, confirm::Bool=options(s).confirm_exit; key="^D") +function edit_abort(s::MIState, confirm::Bool=options(s).confirm_exit; key="^D") set_action!(s, :edit_abort) if !confirm || s.last_action === :edit_abort println(terminal(s)) @@ -2102,9 +2149,9 @@ end const default_keymap = AnyDict( # Tab - '\t' => (s,o...)->edit_tab(s, true), + '\t' => (s::MIState,o...)->edit_tab(s, true), # Enter - '\r' => (s,o...)->begin + '\r' => (s::MIState,o...)->begin if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1) commit_line(s) return :done @@ -2114,13 +2161,13 @@ AnyDict( end, '\n' => KeyAlias('\r'), # Backspace/^H - '\b' => (s,o...) -> is_region_active(s) ? edit_kill_region(s) : edit_backspace(s), + '\b' => (s::MIState,o...) -> is_region_active(s) ? edit_kill_region(s) : edit_backspace(s), 127 => KeyAlias('\b'), # Meta Backspace - "\e\b" => (s,o...)->edit_delete_prev_word(s), + "\e\b" => (s::MIState,o...)->edit_delete_prev_word(s), "\e\x7f" => "\e\b", # ^D - "^D" => (s,o...)->begin + "^D" => (s::MIState,o...)->begin if buffer(s).size > 0 edit_delete(s) else @@ -2128,25 +2175,25 @@ AnyDict( end end, # Ctrl-Space - "\0" => (s,o...)->setmark(s), - "^G" => (s,o...)->(deactivate_region(s); refresh_line(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), - "^P" => (s,o...)->edit_move_up(s), - "^N" => (s,o...)->edit_move_down(s), + "\0" => (s::MIState,o...)->setmark(s), + "^G" => (s::MIState,o...)->(deactivate_region(s); refresh_line(s)), + "^X^X" => (s::MIState,o...)->edit_exchange_point_and_mark(s), + "^B" => (s::MIState,o...)->edit_move_left(s), + "^F" => (s::MIState,o...)->edit_move_right(s), + "^P" => (s::MIState,o...)->edit_move_up(s), + "^N" => (s::MIState,o...)->edit_move_down(s), # Meta-Up - "\e[1;3A" => (s,o...) -> edit_transpose_lines_up!(s), + "\e[1;3A" => (s::MIState,o...) -> edit_transpose_lines_up!(s), # Meta-Down - "\e[1;3B" => (s,o...) -> edit_transpose_lines_down!(s), - "\e[1;2D" => (s,o...)->edit_shift_move(s, edit_move_left), - "\e[1;2C" => (s,o...)->edit_shift_move(s, edit_move_right), - "\e[1;2A" => (s,o...)->edit_shift_move(s, edit_move_up), - "\e[1;2B" => (s,o...)->edit_shift_move(s, edit_move_down), + "\e[1;3B" => (s::MIState,o...) -> edit_transpose_lines_down!(s), + "\e[1;2D" => (s::MIState,o...)->edit_shift_move(s, edit_move_left), + "\e[1;2C" => (s::MIState,o...)->edit_shift_move(s, edit_move_right), + "\e[1;2A" => (s::MIState,o...)->edit_shift_move(s, edit_move_up), + "\e[1;2B" => (s::MIState,o...)->edit_shift_move(s, edit_move_down), # Meta B - "\eb" => (s,o...)->edit_move_word_left(s), + "\eb" => (s::MIState,o...)->edit_move_word_left(s), # Meta F - "\ef" => (s,o...)->edit_move_word_right(s), + "\ef" => (s::MIState,o...)->edit_move_word_right(s), # Ctrl-Left Arrow "\e[1;5D" => "\eb", # Ctrl-Left Arrow on rxvt @@ -2156,29 +2203,29 @@ AnyDict( # Ctrl-Right Arrow on rxvt "\eOc" => "\ef", # Meta Enter - "\e\r" => (s,o...)->edit_insert_newline(s), - "\e." => (s,o...)->edit_insert_last_word(s), + "\e\r" => (s::MIState,o...)->edit_insert_newline(s), + "\e." => (s::MIState,o...)->edit_insert_last_word(s), "\e\n" => "\e\r", - "^_" => (s,o...)->edit_undo!(s), - "\e_" => (s,o...)->edit_redo!(s), + "^_" => (s::MIState,o...)->edit_undo!(s), + "\e_" => (s::MIState,o...)->edit_redo!(s), # Simply insert it into the buffer by default - "*" => (s,data,c)->(edit_insert(s, c)), - "^U" => (s,o...)->edit_kill_line_backwards(s), - "^K" => (s,o...)->edit_kill_line_forwards(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)), - "^E" => (s,o...)->(move_line_end(s); refresh_line(s)), + "*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c)), + "^U" => (s::MIState,o...)->edit_kill_line_backwards(s), + "^K" => (s::MIState,o...)->edit_kill_line_forwards(s), + "^Y" => (s::MIState,o...)->edit_yank(s), + "\ey" => (s::MIState,o...)->edit_yank_pop(s), + "\ew" => (s::MIState,o...)->edit_copy_region(s), + "\eW" => (s::MIState,o...)->edit_kill_region(s), + "^A" => (s::MIState,o...)->(move_line_start(s); refresh_line(s)), + "^E" => (s::MIState,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), + "\e[H" => (s::MIState,o...)->(move_input_start(s); refresh_line(s)), + "\e[F" => (s::MIState,o...)->(move_input_end(s); refresh_line(s)), + "^L" => (s::MIState,o...)->(Terminals.clear(terminal(s)); refresh_line(s)), + "^W" => (s::MIState,o...)->edit_werase(s), # Meta D - "\ed" => (s,o...)->edit_delete_next_word(s), - "^C" => (s,o...)->begin + "\ed" => (s::MIState,o...)->edit_delete_next_word(s), + "^C" => (s::MIState,o...)->begin try # raise the debugger if present ccall(:jl_raise_debugger, Int, ()) catch @@ -2190,60 +2237,60 @@ AnyDict( transition(s, :reset) refresh_line(s) end, - "^Z" => (s,o...)->(return :suspend), + "^Z" => (s::MIState,o...)->(return :suspend), # Right Arrow - "\e[C" => (s,o...)->edit_move_right(s), + "\e[C" => (s::MIState,o...)->edit_move_right(s), # Left Arrow - "\e[D" => (s,o...)->edit_move_left(s), + "\e[D" => (s::MIState,o...)->edit_move_left(s), # Up Arrow - "\e[A" => (s,o...)->edit_move_up(s), + "\e[A" => (s::MIState,o...)->edit_move_up(s), # Down Arrow - "\e[B" => (s,o...)->edit_move_down(s), + "\e[B" => (s::MIState,o...)->edit_move_down(s), # Meta-Right Arrow - "\e[1;3C" => (s,o...) -> edit_indent_right(s, 1), + "\e[1;3C" => (s::MIState,o...) -> edit_indent_right(s, 1), # Meta-Left Arrow - "\e[1;3D" => (s,o...) -> edit_indent_left(s, 1), + "\e[1;3D" => (s::MIState,o...) -> edit_indent_left(s, 1), # Delete - "\e[3~" => (s,o...)->edit_delete(s), + "\e[3~" => (s::MIState,o...)->edit_delete(s), # Bracketed Paste Mode - "\e[200~" => (s,o...)->begin + "\e[200~" => (s::MIState,o...)->begin input = bracketed_paste(s) edit_insert(s, input) end, - "^T" => (s,o...)->edit_transpose_chars(s), - "\et" => (s,o...)->edit_transpose_words(s), - "\eu" => (s,o...)->edit_upper_case(s), - "\el" => (s,o...)->edit_lower_case(s), - "\ec" => (s,o...)->edit_title_case(s), + "^T" => (s::MIState,o...)->edit_transpose_chars(s), + "\et" => (s::MIState,o...)->edit_transpose_words(s), + "\eu" => (s::MIState,o...)->edit_upper_case(s), + "\el" => (s::MIState,o...)->edit_lower_case(s), + "\ec" => (s::MIState,o...)->edit_title_case(s), ) const history_keymap = AnyDict( - "^P" => (s,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)), - "^N" => (s,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)), - "\ep" => (s,o...)->(history_prev(s, mode(s).hist)), - "\en" => (s,o...)->(history_next(s, mode(s).hist)), + "^P" => (s::MIState,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)), + "^N" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)), + "\ep" => (s::MIState,o...)->(history_prev(s, mode(s).hist)), + "\en" => (s::MIState,o...)->(history_next(s, mode(s).hist)), # Up Arrow - "\e[A" => (s,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)), + "\e[A" => (s::MIState,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)), + "\e[B" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)), # Page Up - "\e[5~" => (s,o...)->(history_prev(s, mode(s).hist)), + "\e[5~" => (s::MIState,o...)->(history_prev(s, mode(s).hist)), # Page Down - "\e[6~" => (s,o...)->(history_next(s, mode(s).hist)), - "\e<" => (s,o...)->(history_first(s, mode(s).hist)), - "\e>" => (s,o...)->(history_last(s, mode(s).hist)), + "\e[6~" => (s::MIState,o...)->(history_next(s, mode(s).hist)), + "\e<" => (s::MIState,o...)->(history_first(s, mode(s).hist)), + "\e>" => (s::MIState,o...)->(history_last(s, mode(s).hist)), ) const prefix_history_keymap = merge!( AnyDict( - "^P" => (s,data,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix), - "^N" => (s,data,c)->history_next_prefix(data, data.histprompt.hp, data.prefix), + "^P" => (s::MIState,data::ModeState,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix), + "^N" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix), # Up Arrow - "\e[A" => (s,data,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix), + "\e[A" => (s::MIState,data::ModeState,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), + "\e[B" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix), # by default, pass through to the parent mode - "*" => (s,data,c)->begin + "*" => (s::MIState,data::ModeState,c::StringLike)->begin accept_result(s, data.histprompt); ps = state(s, mode(s)) map = keymap(ps, mode(s)) @@ -2267,42 +2314,42 @@ const prefix_history_keymap = merge!( AnyDict("\e[$(c)l" => "*" for c in 1:20) ) -function setup_prefix_keymap(hp, parent_prompt) +function setup_prefix_keymap(hp::HistoryProvider, parent_prompt::Prompt) p = PrefixHistoryPrompt(hp, parent_prompt) p.keymap_dict = keymap([prefix_history_keymap]) pkeymap = AnyDict( - "^P" => (s,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)), - "^N" => (s,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)), + "^P" => (s::MIState,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)), + "^N" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)), # Up Arrow - "\e[A" => (s,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)), + "\e[A" => (s::MIState,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)), + "\e[B" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)), ) return (p, pkeymap) end -function deactivate(p::TextInterface, s::ModeState, termbuf, term::TextTerminal) +function deactivate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal) clear_input_area(termbuf, s) return s end -function activate(p::TextInterface, s::ModeState, termbuf, term::TextTerminal) +function activate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal) s.ias = InputAreaState(0, 0) refresh_line(s, termbuf) nothing end -function activate(p::TextInterface, s::MIState, termbuf, term::TextTerminal) +function activate(p::TextInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal) @assert p == mode(s) activate(p, state(s), termbuf, term) nothing end -activate(m::ModalInterface, s::MIState, termbuf, term::TextTerminal) = +activate(m::ModalInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal) = activate(mode(s), s, termbuf, term) -commit_changes(t::UnixTerminal, termbuf) = (write(t, take!(termbuf.out_stream)); nothing) +commit_changes(t::UnixTerminal, termbuf::TerminalBuffer) = (write(t, take!(termbuf.out_stream)); nothing) -function transition(f::Function, s::MIState, newmode) +function transition(f::Function, s::MIState, newmode::Union{TextInterface,Symbol}) cancel_beep(s) if newmode === :abort s.aborted = true @@ -2324,7 +2371,7 @@ function transition(f::Function, s::MIState, newmode) commit_changes(t, termbuf) nothing end -transition(s::MIState, mode) = transition((args...)->nothing, s, mode) +transition(s::MIState, mode::Union{TextInterface,Symbol}) = transition((args...)->nothing, s, mode) function reset_state(s::PromptState) if s.input_buffer.size != 0 @@ -2404,7 +2451,7 @@ end empty_undo(s) = nothing -function push_undo(s::PromptState, advance=true) +function push_undo(s::PromptState, advance::Bool=true) resize!(s.undo_buffers, s.undo_idx) s.undo_buffers[end] = copy(s.input_buffer) advance && (s.undo_idx += 1) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 7fd3198f25349..7e6fa7a9d538a 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -32,12 +32,15 @@ import Base: ==, catch_stack +_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int} include("Terminals.jl") using .Terminals abstract type AbstractREPL end +include("options.jl") + include("LineEdit.jl") using .LineEdit import ..LineEdit: @@ -54,7 +57,9 @@ import ..LineEdit: history_search, accept_result, terminal, - MIState + MIState, + PromptState, + TextInterface include("REPLCompletions.jl") using .REPLCompletions @@ -205,7 +210,7 @@ end function display(d::REPLDisplay, mime::MIME"text/plain", x) with_repl_linfo(d.repl) do io - io = IOContext(io, :limit => true, :module => Main) + io = IOContext(io, :limit => true, :module => Main::Module) get(io, :color, false) && write(io, answer_color(d.repl)) if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext) # this can override the :limit property set initially @@ -221,12 +226,12 @@ display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x) function print_response(repl::AbstractREPL, @nospecialize(response), show_value::Bool, have_color::Bool) repl.waserror = response[2] with_repl_linfo(repl) do io - io = IOContext(io, :module => Main) + io = IOContext(io, :module => Main::Module) print_response(io, response, show_value, have_color, specialdisplay(repl)) end return nothing end -function print_response(errio::IO, @nospecialize(response), show_value::Bool, have_color::Bool, specialdisplay=nothing) +function print_response(errio::IO, @nospecialize(response), show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing) Base.sigatomic_begin() val, iserr = response while true @@ -374,64 +379,6 @@ function run_frontend(repl::BasicREPL, backend::REPLBackendRef) nothing end -## User Options - -mutable struct Options - hascolor::Bool - extra_keymap::Union{Dict,Vector{<:Dict}} - # controls the presumed tab width of code pasted into the REPL. - # Must satisfy `0 < tabwidth <= 16`. - tabwidth::Int - # Maximum number of entries in the kill ring queue. - # Beyond this number, oldest entries are discarded first. - kill_ring_max::Int - region_animation_duration::Float64 - beep_duration::Float64 - beep_blink::Float64 - beep_maxduration::Float64 - beep_colors::Vector{String} - beep_use_current::Bool - backspace_align::Bool - backspace_adjust::Bool - confirm_exit::Bool # ^D must be repeated to confirm exit - auto_indent::Bool # indent a newline like line above - auto_indent_tmp_off::Bool # switch auto_indent temporarily off if copy&paste - auto_indent_bracketed_paste::Bool # set to true if terminal knows paste mode - # cancel auto-indent when next character is entered within this time frame : - auto_indent_time_threshold::Float64 - # default IOContext settings at the REPL - iocontext::Dict{Symbol,Any} -end - -Options(; - hascolor = true, - extra_keymap = AnyDict[], - tabwidth = 8, - kill_ring_max = 100, - region_animation_duration = 0.2, - beep_duration = 0.2, beep_blink = 0.2, beep_maxduration = 1.0, - beep_colors = ["\e[90m"], # gray (text_colors not yet available) - beep_use_current = true, - backspace_align = true, backspace_adjust = backspace_align, - confirm_exit = false, - auto_indent = true, - auto_indent_tmp_off = false, - auto_indent_bracketed_paste = false, - auto_indent_time_threshold = 0.005, - iocontext = Dict{Symbol,Any}()) = - Options(hascolor, extra_keymap, tabwidth, - kill_ring_max, region_animation_duration, - beep_duration, beep_blink, beep_maxduration, - beep_colors, beep_use_current, - backspace_align, backspace_adjust, confirm_exit, - auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste, - auto_indent_time_threshold, - iocontext) - -# for use by REPLs not having an options field -const GlobalOptions = Options() - - ## LineEditREPL ## mutable struct LineEditREPL <: AbstractREPL @@ -485,14 +432,14 @@ struct LatexCompletions <: CompletionProvider end beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1]) -function complete_line(c::REPLCompletionProvider, s) +function complete_line(c::REPLCompletionProvider, s::PromptState) partial = beforecursor(s.input_buffer) full = LineEdit.input_string(s) ret, range, should_complete = completions(full, lastindex(partial)) return unique!(map(completion_text, ret)), partial[range], should_complete end -function complete_line(c::ShellCompletionProvider, s) +function complete_line(c::ShellCompletionProvider, s::PromptState) # First parse everything up to the current position partial = beforecursor(s.input_buffer) full = LineEdit.input_string(s) @@ -500,7 +447,7 @@ function complete_line(c::ShellCompletionProvider, s) return unique!(map(completion_text, ret)), partial[range], should_complete end -function complete_line(c::LatexCompletions, s) +function complete_line(c::LatexCompletions, s::PromptState) partial = beforecursor(LineEdit.buffer(s)) full = LineEdit.input_string(s) ret, range, should_complete = bslash_completions(full, lastindex(partial))[2] @@ -543,7 +490,7 @@ munged_history_message(path::String) = """ Invalid history file ($path) format: An editor may have converted tabs to spaces at line """ -function hist_getline(file) +function hist_getline(file::IO) while !eof(file) line = readline(file, keep=true) isempty(line) && return line @@ -552,7 +499,7 @@ function hist_getline(file) return "" end -function hist_from_file(hp, file, path) +function hist_from_file(hp::REPLHistoryProvider, file::IO, path::String) hp.history_file = file seek(file, 0) countlines = 0 @@ -596,7 +543,7 @@ function hist_from_file(hp, file, path) return hp end -function mode_idx(hist::REPLHistoryProvider, mode) +function mode_idx(hist::REPLHistoryProvider, mode::TextInterface) c = :julia for (k,v) in hist.mode_mapping isequal(v, mode) && (c = k) @@ -604,7 +551,7 @@ function mode_idx(hist::REPLHistoryProvider, mode) return c end -function add_history(hist::REPLHistoryProvider, s) +function add_history(hist::REPLHistoryProvider, s::PromptState) str = rstrip(String(take!(copy(s.input_buffer)))) isempty(strip(str)) && return mode = mode_idx(hist, LineEdit.mode(s)) @@ -724,7 +671,7 @@ function history_move_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString, backwards::Bool, - cur_idx = hist.cur_idx) + cur_idx::Int = hist.cur_idx) cur_response = String(take!(copy(LineEdit.buffer(s)))) # when searching forward, start at last_idx if !backwards && hist.last_idx > 0 @@ -732,7 +679,7 @@ function history_move_prefix(s::LineEdit.PrefixSearchState, end hist.last_idx = -1 max_idx = length(hist.history)+1 - idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):max_idx) + idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx) for idx in idxs if (idx == max_idx) || (startswith(hist.history[idx], prefix) && (hist.history[idx] != cur_response || hist.modes[idx] != LineEdit.mode(s))) m = history_move(s, hist, idx) @@ -777,13 +724,10 @@ function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, respo b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1 b = min(lastindex(response_str), b) # ensure that b is valid - searchfunc1, searchfunc2, searchstart, skipfunc = backwards ? - (findlast, findprev, b, prevind) : - (findfirst, findnext, a, nextind) - + searchstart = backwards ? b : a if searchdata == response_str[a:b] if skip_current - searchstart = skipfunc(response_str, searchstart) + searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a) else return true end @@ -792,7 +736,8 @@ function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, respo # Start searching # First the current response buffer if 1 <= searchstart <= lastindex(response_str) - match = searchfunc2(searchdata, response_str, searchstart) + match = backwards ? findprev(searchdata, response_str, searchstart) : + findnext(searchdata, response_str, searchstart) if match !== nothing seek(response_buffer, first(match) - 1) return true @@ -800,10 +745,10 @@ function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, respo end # Now search all the other buffers - idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):length(hist.history)) + idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history)) for idx in idxs h = hist.history[idx] - match = searchfunc1(searchdata, h) + match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h) if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx]) truncate(response_buffer, 0) write(response_buffer, h) @@ -841,12 +786,12 @@ function eval_with_backend(ast, backend::REPLBackendRef) return take!(backend.response_channel) # (val, iserr) end -function respond(f, repl, main; pass_empty = false, suppress_on_semicolon = true) - return function do_respond(s, buf, ok) +function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true) + return function do_respond(s::MIState, buf, ok::Bool) if !ok return transition(s, :abort) end - line = String(take!(buf)) + line = String(take!(buf)::Vector{UInt8}) if !isempty(line) || pass_empty reset(repl) local response @@ -877,7 +822,7 @@ end function mode_keymap(julia_prompt::Prompt) AnyDict( - '\b' => function (s,o...) + '\b' => function (s::MIState,o...) if isempty(s) || position(LineEdit.buffer(s)) == 0 buf = copy(LineEdit.buffer(s)) transition(s, julia_prompt) do @@ -887,7 +832,7 @@ function mode_keymap(julia_prompt::Prompt) LineEdit.edit_backspace(s) end end, - "^C" => function (s,o...) + "^C" => function (s::MIState,o...) LineEdit.move_input_end(s) LineEdit.refresh_line(s) print(LineEdit.terminal(s), "^C\n\n") @@ -1026,7 +971,7 @@ function setup_interface( end repl_keymap = AnyDict( - ';' => function (s,o...) + ';' => function (s::MIState,o...) if isempty(s) || position(LineEdit.buffer(s)) == 0 buf = copy(LineEdit.buffer(s)) transition(s, shell_mode) do @@ -1036,7 +981,7 @@ function setup_interface( edit_insert(s, ';') end end, - '?' => function (s,o...) + '?' => function (s::MIState,o...) if isempty(s) || position(LineEdit.buffer(s)) == 0 buf = copy(LineEdit.buffer(s)) transition(s, help_mode) do @@ -1048,7 +993,7 @@ function setup_interface( end, # Bracketed Paste Mode - "\e[200~" => (s,o...)->begin + "\e[200~" => (s::MIState,o...)->begin input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker sbuffer = LineEdit.buffer(s) curspos = position(sbuffer) @@ -1136,7 +1081,7 @@ function setup_interface( # Open the editor at the location of a stackframe or method # This is accessing a contextual variable that gets set in # the show_backtrace and show_method_table functions. - "^Q" => (s, o...) -> begin + "^Q" => (s::MIState, o...) -> begin linfos = repl.last_shown_line_infos str = String(take!(LineEdit.buffer(s))) n = tryparse(Int, str) @@ -1213,7 +1158,7 @@ input_color(r::StreamREPL) = r.input_color # heuristic function to decide if the presence of a semicolon # at the end of the expression was intended for suppressing output function ends_with_semicolon(line::AbstractString) - match = findlast(isequal(';'), line) + match = findlast(isequal(';'), line)::Union{Nothing,Int} if match !== nothing # state for comment parser, assuming that the `;` isn't in a string or comment # so input like ";#" will still thwart this to give the wrong (anti-conservative) answer @@ -1267,7 +1212,7 @@ function run_frontend(repl::StreamREPL, backend::REPLBackendRef) d = REPLDisplay(repl) dopushdisplay = !in(d,Base.Multimedia.displays) dopushdisplay && pushdisplay(d) - while !eof(repl.stream) + while !eof(repl.stream)::Bool if have_color print(repl.stream,repl.prompt_color) end diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index bf82ece8c7562..dd8d27c96194f 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -55,18 +55,48 @@ struct DictCompletion <: Completion key::String end -completion_text(c::KeywordCompletion) = c.keyword -completion_text(c::PathCompletion) = c.path -completion_text(c::ModuleCompletion) = c.mod -completion_text(c::PackageCompletion) = c.package -completion_text(c::PropertyCompletion) = string(c.property) -completion_text(c::FieldCompletion) = string(c.field) -completion_text(c::MethodCompletion) = sprint(io -> show(io, c.method)) -completion_text(c::BslashCompletion) = c.bslash -completion_text(c::ShellCompletion) = c.text -completion_text(c::DictCompletion) = c.key - -const Completions = Tuple{Vector{Completion}, UnitRange{Int64}, Bool} +# interface definition +function Base.getproperty(c::Completion, name::Symbol) + if name === :keyword + return getfield(c, :keyword)::String + elseif name === :path + return getfield(c, :path)::String + elseif name === :parent + return getfield(c, :parent)::Module + elseif name === :mod + return getfield(c, :mod)::String + elseif name === :package + return getfield(c, :package)::String + elseif name === :property + return getfield(c, :property)::Symbol + elseif name === :field + return getfield(c, :field)::Symbol + elseif name === :method + return getfield(c, :method)::Method + elseif name === :bslash + return getfield(c, :bslash)::String + elseif name === :text + return getfield(c, :text)::String + elseif name === :key + return getfield(c, :key)::String + end + return getfield(c, name) +end + +_completion_text(c::KeywordCompletion) = c.keyword +_completion_text(c::PathCompletion) = c.path +_completion_text(c::ModuleCompletion) = c.mod +_completion_text(c::PackageCompletion) = c.package +_completion_text(c::PropertyCompletion) = string(c.property) +_completion_text(c::FieldCompletion) = string(c.field) +_completion_text(c::MethodCompletion) = sprint(io -> show(io, c.method)) +_completion_text(c::BslashCompletion) = c.bslash +_completion_text(c::ShellCompletion) = c.text +_completion_text(c::DictCompletion) = c.key + +completion_text(c) = _completion_text(c)::String + +const Completions = Tuple{Vector{Completion}, UnitRange{Int}, Bool} function completes_global(x, name) return startswith(x, name) && !('#' in x) @@ -94,7 +124,7 @@ function filtered_mod_names(ffunc::Function, mod::Module, name::AbstractString, end # REPL Symbol Completions -function complete_symbol(sym, ffunc, context_module=Main)::Vector{Completion} +function complete_symbol(sym::String, ffunc, context_module::Module=Main) mod = context_module name = sym @@ -132,14 +162,14 @@ function complete_symbol(sym, ffunc, context_module=Main)::Vector{Completion} # as excluding Main.Main.Main, etc., because that's most likely not what # the user wants p = let mod=mod, modname=nameof(mod) - s->(!Base.isdeprecated(mod, s) && s != modname && ffunc(mod, s)) + s->(!Base.isdeprecated(mod, s) && s != modname && ffunc(mod, s)::Bool) end # Looking for a binding in a module if mod == context_module # Also look in modules we got through `using` - mods = ccall(:jl_module_usings, Any, (Any,), context_module) + mods = ccall(:jl_module_usings, Any, (Any,), context_module)::Vector for m in mods - append!(suggestions, filtered_mod_names(p, m, name)) + append!(suggestions, filtered_mod_names(p, m::Module, name)) end append!(suggestions, filtered_mod_names(p, mod, name, true, true)) else @@ -182,7 +212,7 @@ const sorted_keywords = [ "primitive type", "quote", "return", "struct", "true", "try", "using", "while"] -function complete_keyword(s::Union{String,SubString{String}})::Vector{Completion} +function complete_keyword(s::Union{String,SubString{String}}) r = searchsorted(sorted_keywords, s) i = first(r) n = length(sorted_keywords) @@ -190,10 +220,10 @@ function complete_keyword(s::Union{String,SubString{String}})::Vector{Completion r = first(r):i i += 1 end - map(KeywordCompletion, sorted_keywords[r]) + Completion[KeywordCompletion(kw) for kw in sorted_keywords[r]] end -function complete_path(path::AbstractString, pos; use_envpath=false, shell_escape=false)::Completions +function complete_path(path::AbstractString, pos::Int; use_envpath=false, shell_escape=false) if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path) # if the path is just "~", don't consider the expanded username as a prefix if path == "~" @@ -211,10 +241,10 @@ function complete_path(path::AbstractString, pos; use_envpath=false, shell_escap elseif isdir(dir) files = readdir(dir) else - return PathCompletion[], 0:-1, false + return Completion[], 0:-1, false end catch - return PathCompletion[], 0:-1, false + return Completion[], 0:-1, false end matches = Set{String}() @@ -268,7 +298,7 @@ function complete_path(path::AbstractString, pos; use_envpath=false, shell_escap end end - matchList = PathCompletion[PathCompletion(shell_escape ? replace(s, r"\s" => s"\\\0") : s) for s in matches] + matchList = Completion[PathCompletion(shell_escape ? replace(s, r"\s" => s"\\\0") : s) for s in matches] startpos = pos - lastindex(prefix) + 1 - count(isequal(' '), prefix) # The pos - lastindex(prefix) + 1 is correct due to `lastindex(prefix)-lastindex(prefix)==0`, # hence we need to add one to get the first index. This is also correct when considering @@ -276,9 +306,9 @@ function complete_path(path::AbstractString, pos; use_envpath=false, shell_escap return matchList, startpos:pos, !isempty(matchList) end -function complete_expanduser(path::AbstractString, r)::Completions +function complete_expanduser(path::AbstractString, r) expanded = expanduser(path) - return [PathCompletion(expanded)], r, path != expanded + return Completion[PathCompletion(expanded)], r, path != expanded end # Determines whether method_complete should be tried. It should only be done if @@ -336,7 +366,7 @@ function find_start_brace(s::AbstractString; c_start='(', c_end=')') end braces != 1 && return 0:-1, -1 method_name_end = reverseind(s, i) - startind = nextind(s, something(findprev(in(non_identifier_chars), s, method_name_end), 0)) + startind = nextind(s, something(findprev(in(non_identifier_chars), s, method_name_end), 0))::Int return (startind:lastindex(s), method_name_end) end @@ -436,16 +466,16 @@ function get_type(sym, fn::Module) end # Method completion on function call expression that look like :(max(1)) -function complete_methods(ex_org::Expr, context_module=Main)::Vector{Completion} +function complete_methods(ex_org::Expr, context_module::Module=Main) args_ex = Any[] - func, found = get_value(ex_org.args[1], context_module) + func, found = get_value(ex_org.args[1], context_module)::Tuple{Any,Bool} !found && return Completion[] funargs = ex_org.args[2:end] # handle broadcasting, but only handle number of arguments instead of # argument types if ex_org.head === :. && ex_org.args[2] isa Expr - for _ in ex_org.args[2].args + for _ in (ex_org.args[2]::Expr).args push!(args_ex, Any) end else @@ -463,7 +493,7 @@ function complete_methods(ex_org::Expr, context_module=Main)::Vector{Completion} ms = method.sig # Check if the method's type signature intersects the input types - if typeintersect(Base.rewrap_unionall(Tuple{Base.unwrap_unionall(ms).parameters[1 : min(na, end)]...}, ms), t_in) != Union{} + if typeintersect(Base.rewrap_unionall(Tuple{(Base.unwrap_unionall(ms)::DataType).parameters[1 : min(na, end)]...}, ms), t_in) !== Union{} push!(out, MethodCompletion(func, t_in, method)) end end @@ -493,7 +523,7 @@ function afterusing(string::String, startpos::Int) return occursin(r"^\b(using|import)\s*((\w+[.])*\w+\s*,\s*)*$", str[fr:end]) end -function bslash_completions(string, pos)::Tuple{Bool, Completions} +function bslash_completions(string::String, pos::Int) slashpos = something(findprev(isequal('\\'), string, pos), 0) if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos && !(1 < slashpos && (string[prevind(string, slashpos)]=='\\'))) @@ -501,26 +531,25 @@ function bslash_completions(string, pos)::Tuple{Bool, Completions} s = string[slashpos:pos] latex = get(latex_symbols, s, "") if !isempty(latex) # complete an exact match - return (true, ([BslashCompletion(latex)], slashpos:pos, true)) + return (true, (Completion[BslashCompletion(latex)], slashpos:pos, true)) end emoji = get(emoji_symbols, s, "") if !isempty(emoji) - return (true, ([BslashCompletion(emoji)], slashpos:pos, true)) + return (true, (Completion[BslashCompletion(emoji)], slashpos:pos, true)) end # return possible matches; these cannot be mixed with regular # Julian completions as only latex / emoji symbols contain the leading \ if startswith(s, "\\:") # emoji - emoji_names = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols)) - return (true, (map(BslashCompletion, sort!(collect(emoji_names))), slashpos:pos, true)) + namelist = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols)) else # latex - latex_names = Iterators.filter(k -> startswith(k, s), keys(latex_symbols)) - return (true, (map(BslashCompletion, sort!(collect(latex_names))), slashpos:pos, true)) + namelist = Iterators.filter(k -> startswith(k, s), keys(latex_symbols)) end + return (true, (Completion[BslashCompletion(name) for name in sort!(collect(namelist))], slashpos:pos, true)) end return (false, (Completion[], 0:-1, false)) end -function dict_identifier_key(str, tag, context_module = Main) +function dict_identifier_key(str::String, tag::Symbol, context_module::Module = Main) if tag === :string str_close = str*"\"" elseif tag === :cmd @@ -538,14 +567,14 @@ function dict_identifier_key(str, tag, context_module = Main) isdefined(obj, sym) || return (nothing, nothing, nothing) obj = getfield(obj, sym) end - (isa(obj, AbstractDict) && length(obj) < 1_000_000) || return (nothing, nothing, nothing) + (isa(obj, AbstractDict) && length(obj)::Int < 1_000_000) || return (nothing, nothing, nothing) begin_of_key = something(findnext(!isspace, str, nextind(str, end_of_identifier) + 1), # +1 for [ lastindex(str)+1) - return (obj, str[begin_of_key:end], begin_of_key) + return (obj::AbstractDict, str[begin_of_key:end], begin_of_key) end # This needs to be a separate non-inlined function, see #19441 -@noinline function find_dict_matches(identifier, partial_key) +@noinline function find_dict_matches(identifier::AbstractDict, partial_key) matches = String[] for key in keys(identifier) rkey = repr(key) @@ -554,7 +583,7 @@ end return matches end -function project_deps_get_completion_candidates(pkgstarts::String, project_file::String)::Vector{Completion} +function project_deps_get_completion_candidates(pkgstarts::String, project_file::String) loading_candidates = String[] open(project_file) do io state = :top @@ -577,7 +606,7 @@ function project_deps_get_completion_candidates(pkgstarts::String, project_file: return Completion[PackageCompletion(name) for name in loading_candidates] end -function completions(string, pos, context_module=Main)::Completions +function completions(string::String, pos::Int, context_module::Module=Main) # First parse everything up to the current position partial = string[1:pos] inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false)) @@ -587,7 +616,7 @@ function completions(string, pos, context_module=Main)::Completions if identifier !== nothing matches = find_dict_matches(identifier, partial_key) length(matches)==1 && (lastindex(string) <= pos || string[nextind(string,pos)] != ']') && (matches[1]*=']') - length(matches)>0 && return [DictCompletion(identifier, match) for match in sort!(matches)], loc:pos, true + length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true end # otherwise... @@ -618,7 +647,7 @@ function completions(string, pos, context_module=Main)::Completions ok && return ret # Make sure that only bslash_completions is working on strings - inc_tag==:string && return String[], 0:-1, false + inc_tag==:string && return Completion[], 0:-1, false if inc_tag === :other && should_method_complete(partial) frange, method_name_end = find_start_brace(partial) # strip preceding ! operator @@ -706,7 +735,7 @@ function completions(string, pos, context_module=Main)::Completions frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end) startpos = first(frange) i = prevind(string, startpos) - elseif c in ["\'\"\`"...] + elseif c in ('\'', '\"', '\`') s = "$c$c"*string[startpos:pos] break else @@ -719,26 +748,28 @@ function completions(string, pos, context_module=Main)::Completions return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true end -function shell_completions(string, pos)::Completions +function shell_completions(string, pos) # First parse everything up to the current position scs = string[1:pos] local args, last_parse try - args, last_parse = Base.shell_parse(scs, true) + args, last_parse = Base.shell_parse(scs, true)::Tuple{Expr,UnitRange{Int}} catch return Completion[], 0:-1, false end + ex = args.args[end]::Expr # Now look at the last thing we parsed - isempty(args.args[end].args) && return Completion[], 0:-1, false - arg = args.args[end].args[end] - if all(s -> isa(s, AbstractString), args.args[end].args) + isempty(ex.args) && return Completion[], 0:-1, false + arg = ex.args[end] + if all(s -> isa(s, AbstractString), ex.args) + arg = arg::AbstractString # Treat this as a path # As Base.shell_parse throws away trailing spaces (unless they are escaped), # we need to special case here. # If the last char was a space, but shell_parse ignored it search on "". ignore_last_word = arg != " " && scs[end] == ' ' - prefix = ignore_last_word ? "" : join(args.args[end].args) + prefix = ignore_last_word ? "" : join(ex.args) # Also try looking into the env path if the user wants to complete the first argument use_envpath = !ignore_last_word && length(args.args) < 2 diff --git a/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl b/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl index 2b5676cb77706..c6fc1989b1c39 100644 --- a/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl +++ b/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl @@ -51,6 +51,16 @@ subtypes. """ abstract type AbstractMenu end +function getproperty(m::AbstractMenu, name::Symbol) + if name === :pagesize + return getfield(m, :pagesize)::Int + elseif name === :pageoffset + return getfield(m, :pageoffset)::Int + end + return getfield(m, name) +end + + # TODO Julia2.0: get rid of parametric intermediate, making it just # abstract type ConfiguredMenu <: AbstractMenu end # Or perhaps just make all menus ConfiguredMenus @@ -297,10 +307,10 @@ overwriting of the previous display. On older versions of Julia, this was called `printMenu` and it lacked the `state` argument/return value. This older function is supported on all Julia 1.x versions but will be dropped in Julia 2.0. """ -function printmenu(out, m::AbstractMenu, cursoridx::Int; oldstate=nothing, init::Bool=false) +function printmenu(out::IO, m::AbstractMenu, cursoridx::Int; oldstate=nothing, init::Bool=false) # TODO Julia 2.0?: get rid of `init` and just use `oldstate` buf = IOBuffer() - lastoption = numoptions(m) + lastoption = numoptions(m)::Int ncleared = oldstate === nothing ? m.pagesize-1 : oldstate if init @@ -320,11 +330,11 @@ function printmenu(out, m::AbstractMenu, cursoridx::Int; oldstate=nothing, init: downscrollable = i == lastline && i != lastoption if upscrollable && downscrollable - print(buf, updown_arrow(m)) + print(buf, updown_arrow(m)::Union{Char,String}) elseif upscrollable - print(buf, up_arrow(m)) + print(buf, up_arrow(m)::Union{Char,String}) elseif downscrollable - print(buf, down_arrow(m)) + print(buf, down_arrow(m)::Union{Char,String}) else print(buf, ' ') end diff --git a/stdlib/REPL/src/TerminalMenus/util.jl b/stdlib/REPL/src/TerminalMenus/util.jl index 68d98dd57c199..8ad9ec0e4100d 100644 --- a/stdlib/REPL/src/TerminalMenus/util.jl +++ b/stdlib/REPL/src/TerminalMenus/util.jl @@ -12,13 +12,13 @@ PAGE_UP, PAGE_DOWN) -readbyte(stream::IO=stdin) = Char(read(stream,1)[1]) +readbyte(stream::IO=stdin) = read(stream, Char) # Read the next key from stdin. It is also able to read several bytes for # escaped keys such as the arrow keys, home/end keys, etc. # Escaped keys are returned using the `Key` enum. -readkey(stream::IO=stdin) = UInt32(_readkey(stream)) -function _readkey(stream::IO=stdin) +readkey(stream::Base.LibuvStream=stdin) = UInt32(_readkey(stream)) +function _readkey(stream::Base.LibuvStream=stdin) c = readbyte(stream) # Escape characters diff --git a/stdlib/REPL/src/Terminals.jl b/stdlib/REPL/src/Terminals.jl index 28a6dd828bfa8..dac19406b3fc1 100644 --- a/stdlib/REPL/src/Terminals.jl +++ b/stdlib/REPL/src/Terminals.jl @@ -76,8 +76,8 @@ cmove_col(t::TextTerminal, c) = cmove(c, getY(t)) hascolor(::TextTerminal) = false # Utility Functions -width(t::TextTerminal) = displaysize(t)[2] -height(t::TextTerminal) = displaysize(t)[1] +width(t::TextTerminal) = (displaysize(t)::Tuple{Int,Int})[2] +height(t::TextTerminal) = (displaysize(t)::Tuple{Int,Int})[1] # For terminals with buffers flush(t::TextTerminal) = nothing @@ -160,6 +160,6 @@ Base.haskey(t::TTYTerminal, key) = haskey(pipe_writer(t), key) Base.getindex(t::TTYTerminal, key) = getindex(pipe_writer(t), key) Base.get(t::TTYTerminal, key, default) = get(pipe_writer(t), key, default) -Base.peek(t::TTYTerminal, ::Type{T}) where {T} = peek(t.in_stream, T) +Base.peek(t::TTYTerminal, ::Type{T}) where {T} = peek(t.in_stream, T)::T end # module diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index 8246f4380087a..3593ad3c089e7 100644 --- a/stdlib/REPL/src/docview.jl +++ b/stdlib/REPL/src/docview.jl @@ -51,7 +51,7 @@ function _helpmode(io::IO, line::AbstractString) # keyword such as `function` would throw a parse error due to the missing `end`. assym elseif isexpr(x, (:using, :import)) - x.head + (x::Expr).head else # Retrieving docs for macros requires us to make a distinction between the text # `@macroname` and `@macroname()`. These both parse the same, but are used by @@ -67,7 +67,7 @@ end _helpmode(line::AbstractString) = _helpmode(stdout, line) # Print vertical lines along each docstring if there are multiple docs -function insert_hlines(io::IO, docs) +function insert_hlines(io::IO, docs::Markdown.MD) if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results]) return docs end @@ -250,13 +250,13 @@ function summarize(binding::Binding, sig) return md end -function summarize(io::IO, λ::Function, binding) +function summarize(io::IO, λ::Function, binding::Binding) kind = startswith(string(binding.var), '@') ? "macro" : "`Function`" println(io, "`", binding, "` is a ", kind, ".") println(io, "```\n", methods(λ), "\n```") end -function summarize(io::IO, T::DataType, binding) +function summarize(io::IO, T::DataType, binding::Binding) println(io, "# Summary") println(io, "```") println(io, @@ -292,11 +292,11 @@ function summarize(io::IO, T::DataType, binding) end end -function summarize(io::IO, m::Module, binding) +function summarize(io::IO, m::Module, binding::Binding) println(io, "No docstring found for module `", m, "`.\n") end -function summarize(io::IO, @nospecialize(T), binding) +function summarize(io::IO, @nospecialize(T), binding::Binding) T = typeof(T) println(io, "`", binding, "` is of type `", T, "`.\n") summarize(io, T, binding) @@ -304,10 +304,10 @@ end # repl search and completions for help -function repl_search(io::IO, s) +function repl_search(io::IO, s::Union{Symbol,String}) pre = "search:" print(io, pre) - printmatches(io, s, doc_completions(s), cols = displaysize(io)[2] - length(pre)) + printmatches(io, s, doc_completions(s), cols = _displaysize(io)[2] - length(pre)) println(io, "\n") end repl_search(s) = repl_search(stdout, s) @@ -358,7 +358,7 @@ function repl_latex(io::IO, s::String) end repl_latex(s::String) = repl_latex(stdout, s) -macro repl(ex, brief=false) repl(ex; brief=brief) end +macro repl(ex, brief::Bool=false) repl(ex; brief=brief) end macro repl(io, ex, brief) repl(io, ex; brief=brief) end function repl(io::IO, s::Symbol; brief::Bool=true) @@ -378,10 +378,11 @@ repl(io::IO, str::AbstractString; brief::Bool=true) = :(apropos($io, $str)) repl(io::IO, other; brief::Bool=true) = esc(:(@doc $other)) #repl(io::IO, other) = lookup_doc(other) # TODO -repl(x; brief=true) = repl(stdout, x; brief=brief) +repl(x; brief::Bool=true) = repl(stdout, x; brief=brief) -function _repl(x, brief=true) +function _repl(x, brief::Bool=true) if isexpr(x, :call) + x = x::Expr # determine the types of the values kwargs = nothing pargs = Any[] @@ -482,7 +483,7 @@ fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), fiel # Fuzzy Search Algorithm -function matchinds(needle, haystack; acronym = false) +function matchinds(needle, haystack; acronym::Bool = false) chars = collect(needle) is = Int[] lastc = '\0' @@ -519,8 +520,8 @@ function fuzzyscore(needle, haystack) return score end -function fuzzysort(search, candidates) - scores = map(cand -> (fuzzyscore(search, cand), -levenshtein(search, cand)), candidates) +function fuzzysort(search::String, candidates::Vector{String}) + scores = map(cand -> (fuzzyscore(search, cand), -Float64(levenshtein(search, cand))), candidates) candidates[sortperm(scores)] |> reverse end @@ -544,8 +545,8 @@ function levenshtein(s1, s2) return d[m+1, n+1] end -function levsort(search, candidates) - scores = map(cand -> (levenshtein(search, cand), -fuzzyscore(search, cand)), candidates) +function levsort(search::String, candidates::Vector{String}) + scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates) candidates = candidates[sortperm(scores)] i = 0 for outer i = 1:length(candidates) @@ -569,7 +570,7 @@ end printmatch(args...) = printfuzzy(stdout, args...) -function printmatches(io::IO, word, matches; cols = displaysize(io)[2]) +function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2]) total = 0 for match in matches total + length(match) + 1 > cols && break @@ -580,9 +581,9 @@ function printmatches(io::IO, word, matches; cols = displaysize(io)[2]) end end -printmatches(args...; cols = displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols) +printmatches(args...; cols::Int = _displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols) -function print_joined_cols(io::IO, ss, delim = "", last = delim; cols = displaysize(io)[2]) +function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2]) i = 0 total = 0 for outer i = 1:length(ss) @@ -592,13 +593,13 @@ function print_joined_cols(io::IO, ss, delim = "", last = delim; cols = displays join(io, ss[1:i], delim, last) end -print_joined_cols(args...; cols = displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols) +print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols) -function print_correction(io, word) +function print_correction(io::IO, word::String) cors = levsort(word, accessible(Main)) pre = "Perhaps you meant " print(io, pre) - print_joined_cols(io, cors, ", ", " or "; cols = displaysize(io)[2] - length(pre)) + print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre)) println(io) return end diff --git a/stdlib/REPL/src/options.jl b/stdlib/REPL/src/options.jl new file mode 100644 index 0000000000000..6f9da0a9c9029 --- /dev/null +++ b/stdlib/REPL/src/options.jl @@ -0,0 +1,56 @@ +## User Options + +mutable struct Options + hascolor::Bool + extra_keymap::Union{Dict,Vector{<:Dict}} + # controls the presumed tab width of code pasted into the REPL. + # Must satisfy `0 < tabwidth <= 16`. + tabwidth::Int + # Maximum number of entries in the kill ring queue. + # Beyond this number, oldest entries are discarded first. + kill_ring_max::Int + region_animation_duration::Float64 + beep_duration::Float64 + beep_blink::Float64 + beep_maxduration::Float64 + beep_colors::Vector{String} + beep_use_current::Bool + backspace_align::Bool + backspace_adjust::Bool + confirm_exit::Bool # ^D must be repeated to confirm exit + auto_indent::Bool # indent a newline like line above + auto_indent_tmp_off::Bool # switch auto_indent temporarily off if copy&paste + auto_indent_bracketed_paste::Bool # set to true if terminal knows paste mode + # cancel auto-indent when next character is entered within this time frame : + auto_indent_time_threshold::Float64 + # default IOContext settings at the REPL + iocontext::Dict{Symbol,Any} +end + +Options(; + hascolor = true, + extra_keymap = AnyDict[], + tabwidth = 8, + kill_ring_max = 100, + region_animation_duration = 0.2, + beep_duration = 0.2, beep_blink = 0.2, beep_maxduration = 1.0, + beep_colors = ["\e[90m"], # gray (text_colors not yet available) + beep_use_current = true, + backspace_align = true, backspace_adjust = backspace_align, + confirm_exit = false, + auto_indent = true, + auto_indent_tmp_off = false, + auto_indent_bracketed_paste = false, + auto_indent_time_threshold = 0.005, + iocontext = Dict{Symbol,Any}()) = + Options(hascolor, extra_keymap, tabwidth, + kill_ring_max, region_animation_duration, + beep_duration, beep_blink, beep_maxduration, + beep_colors, beep_use_current, + backspace_align, backspace_adjust, confirm_exit, + auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste, + auto_indent_time_threshold, + iocontext) + +# for use by REPLs not having an options field +const GlobalOptions = Options() diff --git a/stdlib/REPL/test/lineedit.jl b/stdlib/REPL/test/lineedit.jl index 6442687d6f8de..50ca0f23e2414 100644 --- a/stdlib/REPL/test/lineedit.jl +++ b/stdlib/REPL/test/lineedit.jl @@ -242,21 +242,22 @@ end buf = IOBuffer("a\na\na\n") seek(buf, 0) for i = 1:6 - LineEdit.edit_move_right(buf) + @test LineEdit.edit_move_right(buf) @test position(buf) == i end @test eof(buf) +@test !LineEdit.edit_move_right(buf) for i = 5:-1:0 - LineEdit.edit_move_left(buf) + @test @inferred LineEdit.edit_move_left(buf) @test position(buf) == i end # skip unicode combining characters buf = IOBuffer("ŷ") seek(buf, 0) -LineEdit.edit_move_right(buf) +@test LineEdit.edit_move_right(buf) @test eof(buf) -LineEdit.edit_move_left(buf) +@test LineEdit.edit_move_left(buf) @test position(buf) == 0 ## edit_move_{up,down} ## @@ -307,7 +308,7 @@ seek(buf,0) buf = IOBuffer("type X\n ") seekend(buf) -@test !isempty(LineEdit.edit_delete_prev_word(buf)) +@test !isempty(@inferred(LineEdit.edit_delete_prev_word(buf))) @test position(buf) == 5 @test buf.size == 5 @test content(buf) == "type " @@ -323,7 +324,7 @@ buf = IOBuffer("x = func(arg1,arg2 , arg3)") seekend(buf) LineEdit.char_move_word_left(buf) @test position(buf) == 21 -@test !isempty(LineEdit.edit_delete_prev_word(buf)) +@test !isempty(@inferred(LineEdit.edit_delete_prev_word(buf))) @test content(buf) == "x = func(arg1,arg3)" @test !isempty(LineEdit.edit_delete_prev_word(buf)) @test content(buf) == "x = func(arg3)" @@ -336,18 +337,18 @@ let buf = IOBuffer() LineEdit.edit_move_left(buf) @test position(buf) == 0 LineEdit.edit_move_right(buf) - @test bytesavailable(buf) == 0 - LineEdit.edit_backspace(buf, false, false) + @test @inferred(bytesavailable(buf)) == 0 + @inferred(LineEdit.edit_backspace(buf, false, false)) @test content(buf) == "a" end ## edit_transpose_chars ## let buf = IOBuffer() - edit_insert(buf, "abcde") + @inferred(edit_insert(buf, "abcde")) seek(buf,0) - LineEdit.edit_transpose_chars(buf) + @inferred(LineEdit.edit_transpose_chars(buf)) @test content(buf) == "abcde" - LineEdit.char_move_right(buf) + @inferred Union{Char,Bool} LineEdit.char_move_right(buf) LineEdit.edit_transpose_chars(buf) @test content(buf) == "bacde" LineEdit.edit_transpose_chars(buf) @@ -359,7 +360,7 @@ let buf = IOBuffer() @test content(buf) == "bcade" seek(buf, 0) - LineEdit.edit_clear(buf) + @inferred(LineEdit.edit_clear(buf)) edit_insert(buf, "αβγδε") seek(buf,0) LineEdit.edit_transpose_chars(buf) @@ -385,7 +386,7 @@ end mode[] = :readline edit_insert(buf, "àbç def gh ") - @test transpose!(0) == ("àbç def gh ", 0) + @test @inferred(transpose!(0)) == ("àbç def gh ", 0) @test transpose!(1) == ("àbç def gh ", 1) @test transpose!(2) == ("àbç def gh ", 2) @test transpose!(3) == ("def àbç gh ", 7) @@ -395,7 +396,7 @@ end @test transpose!(7) == ("àbç gh def ", 11) @test transpose!(10) == ("àbç def gh ", 11) @test transpose!(11) == ("àbç gh def", 12) - edit_insert(buf, " ") + @inferred(edit_insert(buf, " ")) @test transpose!(13) == ("àbç def gh", 13) take!(buf) @@ -412,7 +413,7 @@ end let s = new_state(), buf = buffer(s) - edit_insert(s,"first line\nsecond line\nthird line") + @inferred Union{Int,LineEdit.InputAreaState} edit_insert(s,"first line\nsecond line\nthird line") @test content(buf) == "first line\nsecond line\nthird line" ## edit_move_line_start/end ## @@ -438,14 +439,14 @@ let s = new_state(), ## edit_kill_line, edit_yank ## seek(buf, 0) - LineEdit.edit_kill_line(s) + @inferred Union{Symbol,LineEdit.InputAreaState} LineEdit.edit_kill_line(s) s.key_repeats = 1 # Manually flag a repeated keypress LineEdit.edit_kill_line(s) s.key_repeats = 0 @test content(buf) == "second line\nthird line" LineEdit.move_line_end(s) LineEdit.edit_move_right(s) - LineEdit.edit_yank(s) + @inferred Union{Symbol,LineEdit.InputAreaState} LineEdit.edit_yank(s) @test content(buf) == "second line\nfirst line\nthird line" end @@ -460,7 +461,7 @@ let buf = IOBuffer( outbuf = IOBuffer() termbuf = REPL.Terminals.TerminalBuffer(outbuf) term = FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer()) - s = LineEdit.refresh_multi_line(termbuf, term, buf, + s = @inferred LineEdit.refresh_multi_line(termbuf, term, buf, REPL.LineEdit.InputAreaState(0,0), "julia> ", indent = 7) @test s == REPL.LineEdit.InputAreaState(3,1) end @@ -468,9 +469,9 @@ end @testset "function prompt indentation" begin local s, term, ps, buf, outbuf, termbuf s = new_state() - term = REPL.LineEdit.terminal(s) + term = @inferred REPL.AbstractTerminal REPL.LineEdit.terminal(s) # default prompt: PromptState.indent should not be set to a final fixed value - ps::LineEdit.PromptState = s.mode_state[s.current_mode] + ps::LineEdit.PromptState = @inferred LineEdit.ModeState s.mode_state[s.current_mode] @test ps.indent == -1 # the prompt is modified afterwards to a function ps.p.prompt = let i = 0 @@ -480,7 +481,7 @@ end write(buf, "begin\n julia = :fun\nend") outbuf = IOBuffer() termbuf = REPL.Terminals.TerminalBuffer(outbuf) - LineEdit.refresh_multi_line(termbuf, term, ps) + @inferred(LineEdit.refresh_multi_line(termbuf, term, ps)) @test String(take!(outbuf)) == "\r\e[0K\e[1mJulia is Fun! > \e[0m\r\e[16Cbegin\n" * "\r\e[16C julia = :fun\n" * @@ -496,13 +497,13 @@ end s = new_state() edit_insert(s, "αä🐨") # for issue #28183 s.current_action = :unknown - LineEdit.edit_shift_move(s, LineEdit.edit_move_left) + @inferred Union{Bool, LineEdit.InputAreaState} LineEdit.edit_shift_move(s, LineEdit.edit_move_left) @test LineEdit.region(s) == (5=>9) LineEdit.edit_shift_move(s, LineEdit.edit_move_left) @test LineEdit.region(s) == (2=>9) LineEdit.edit_shift_move(s, LineEdit.edit_move_left) @test LineEdit.region(s) == (0=>9) - LineEdit.edit_shift_move(s, LineEdit.edit_move_right) + @inferred Union{Bool, LineEdit.InputAreaState} LineEdit.edit_shift_move(s, LineEdit.edit_move_right) @test LineEdit.region(s) == (2=>9) end @@ -513,9 +514,9 @@ end end edit_insert(s, "for x=1:10\n") - LineEdit.edit_tab(s) + @inferred Union{Symbol,LineEdit.InputAreaState} LineEdit.edit_tab(s) @test content(s) == "for x=1:10\n " - LineEdit.edit_backspace(s, true, false) + @inferred Union{Nothing,LineEdit.InputAreaState} LineEdit.edit_backspace(s, true, false) @test content(s) == "for x=1:10\n" edit_insert(s, " ") @test position(s) == 13 @@ -608,11 +609,11 @@ end local buf = IOBuffer() edit_insert(buf, "aa bB CC") seekstart(buf) - LineEdit.edit_upper_case(buf) - LineEdit.edit_title_case(buf) + @inferred LineEdit.edit_upper_case(buf) + @inferred LineEdit.edit_title_case(buf) @test String(take!(copy(buf))) == "AA Bb CC" @test position(buf) == 5 - LineEdit.edit_lower_case(buf) + @inferred LineEdit.edit_lower_case(buf) @test String(take!(copy(buf))) == "AA Bb cc" end @@ -621,18 +622,18 @@ end s = new_state() buf = buffer(s) edit_insert(s, "ça ≡ nothing") - @test transform!(LineEdit.edit_copy_region, s) == ("ça ≡ nothing", 12, 0) + @test @inferred 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) + @test @inferred 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 @inferred 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) + @test @inferred transform!(LineEdit.edit_yank, s) == ("a ≡ notçhing", 7, 0) s.last_action = :unknown # 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) + @test @inferred transform!(LineEdit.edit_yank_pop, s) == ("a ≡ notçhing", 7, 0) s.last_action = :edit_yank # now this should work: @test transform!(LineEdit.edit_yank_pop, s) == ("ça ≡ nothingçhing", 12, 0) @@ -689,13 +690,13 @@ end edit_insert(s, "one two three") - @test edit!(LineEdit.edit_delete_prev_word) == "one two " - @test edit!(edit_undo!) == "one two three" + @test @inferred edit!(LineEdit.edit_delete_prev_word) == "one two " + @test @inferred edit!(edit_undo!) == "one two three" @test edit!(edit_redo!) == "one two " @test edit!(edit_undo!) == "one two three" edit_insert(s, " four") - @test edit!(s->edit_insert(s, " five")) == "one two three four five" + @test @inferred edit!(s->edit_insert(s, " five")) == "one two three four five" @test edit!(edit_undo!) == "one two three four" @test edit!(edit_undo!) == "one two three" @test edit!(edit_redo!) == "one two three four" @@ -703,16 +704,16 @@ end @test edit!(edit_undo!) == "one two three four" @test edit!(edit_undo!) == "one two three" - @test edit!(LineEdit.edit_clear) == "" + @test @inferred edit!(LineEdit.edit_clear) == "" @test edit!(LineEdit.edit_clear) == "" # should not be saved twice @test edit!(edit_undo!) == "one two three" - @test edit!(LineEdit.edit_insert_newline) == "one two three\n" + @test @inferred edit!(LineEdit.edit_insert_newline) == "one two three\n" @test edit!(edit_undo!) == "one two three" LineEdit.edit_move_left(s) LineEdit.edit_move_left(s) - @test edit!(LineEdit.edit_transpose_chars) == "one two there" + @test @inferred edit!(LineEdit.edit_transpose_chars) == "one two there" @test edit!(edit_undo!) == "one two three" @test edit!(LineEdit.edit_transpose_words) == "one three two" @test edit!(edit_undo!) == "one two three" @@ -808,8 +809,8 @@ end local buf = IOBuffer() write(buf, "1\n22\n333") seek(buf, 0) - @test LineEdit.edit_indent(buf, -1, false) == false - @test transform!(buf->LineEdit.edit_indent(buf, -1, false), buf) == ("1\n22\n333", 0, 0) + @test @inferred LineEdit.edit_indent(buf, -1, false) == false + @test @inferred transform!(buf->LineEdit.edit_indent(buf, -1, false), buf) == ("1\n22\n333", 0, 0) @test transform!(buf->LineEdit.edit_indent(buf, +1, false), buf) == (" 1\n22\n333", 1, 0) @test transform!(buf->LineEdit.edit_indent(buf, +2, false), buf) == (" 1\n22\n333", 3, 0) @test transform!(buf->LineEdit.edit_indent(buf, -2, false), buf) == (" 1\n22\n333", 1, 0) @@ -842,10 +843,10 @@ end end @testset "edit_transpose_lines_{up,down}!" begin - transpose_lines_up!(buf) = LineEdit.edit_transpose_lines_up!(buf, position(buf)=>position(buf)) - transpose_lines_up_reg!(buf) = LineEdit.edit_transpose_lines_up!(buf, region(buf)) - transpose_lines_down!(buf) = LineEdit.edit_transpose_lines_down!(buf, position(buf)=>position(buf)) - transpose_lines_down_reg!(buf) = LineEdit.edit_transpose_lines_down!(buf, region(buf)) + transpose_lines_up!(buf) = @inferred LineEdit.edit_transpose_lines_up!(buf, position(buf)=>position(buf)) + transpose_lines_up_reg!(buf) = @inferred LineEdit.edit_transpose_lines_up!(buf, region(buf)) + transpose_lines_down!(buf) = @inferred LineEdit.edit_transpose_lines_down!(buf, position(buf)=>position(buf)) + transpose_lines_down_reg!(buf) = @inferred LineEdit.edit_transpose_lines_down!(buf, region(buf)) local buf buf = IOBuffer() @@ -881,7 +882,7 @@ end end @testset "edit_insert_last_word" begin - get_last_word(str::String) = LineEdit.get_last_word(IOBuffer(str)) + get_last_word(str::String) = @inferred LineEdit.get_last_word(IOBuffer(str)) @test get_last_word("1+2") == "2" @test get_last_word("1+23") == "23" @test get_last_word("1+2") == "2" diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index 6044040e22962..a0d614edce5c1 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -96,10 +96,10 @@ function map_completion_text(completions) return map(completion_text, c), r, res end -test_complete(s) = map_completion_text(completions(s,lastindex(s))) -test_scomplete(s) = map_completion_text(shell_completions(s,lastindex(s))) -test_bslashcomplete(s) = map_completion_text(bslash_completions(s,lastindex(s))[2]) -test_complete_context(s) = map_completion_text(completions(s,lastindex(s),Main.CompletionFoo)) +test_complete(s) = map_completion_text(@inferred(completions(s,lastindex(s)))) +test_scomplete(s) = map_completion_text(@inferred(shell_completions(s,lastindex(s)))) +test_bslashcomplete(s) = map_completion_text(@inferred(bslash_completions(s,lastindex(s)))[2]) +test_complete_context(s) = map_completion_text(@inferred(completions(s,lastindex(s),Main.CompletionFoo))) module M32377 end test_complete_32377(s) = map_completion_text(completions(s,lastindex(s), M32377)) @@ -135,7 +135,7 @@ end let s = "Main.CompletionFoo." c, r = test_complete(s) @test "bar" in c - @test r === UnitRange{Int64}(20:19) + @test r === 20:19 @test s[r] == "" end @@ -596,7 +596,7 @@ end let c, r, res c, r, res = test_scomplete("\$a") @test c == String[] - @test r === UnitRange{Int64}(0:-1) + @test r === 0:-1 @test res === false end @@ -646,7 +646,7 @@ let s, c, r s = "/tmp/" c,r = test_scomplete(s) @test !("tmp/" in c) - @test r === UnitRange{Int64}(6:5) + @test r === 6:5 @test s[r] == "" end @@ -663,7 +663,7 @@ let s, c, r file = joinpath(path, "repl completions") s = "/tmp " c,r = test_scomplete(s) - @test r === UnitRange{Int64}(6:5) + @test r === 6:5 end # Test completing paths with an escaped trailing space @@ -977,7 +977,7 @@ end let s = "" c, r = test_complete_context(s) @test "bar" in c - @test r === UnitRange{Int64}(1:0) + @test r === 1:0 @test s[r] == "" end diff --git a/test/iostream.jl b/test/iostream.jl index 0d1735fb2167c..e49386913a120 100644 --- a/test/iostream.jl +++ b/test/iostream.jl @@ -166,3 +166,8 @@ end @test length(readavailable(io)) > 0 end end + +@testset "inference" begin + @test all(T -> T <: Union{UInt, Int}, Base.return_types(unsafe_write, (IO, Ptr{UInt8}, UInt))) + @test all(T -> T === Bool, Base.return_types(eof, (IO,))) +end diff --git a/test/strings/basic.jl b/test/strings/basic.jl index 465075cb88a3d..8dd54a40c0862 100644 --- a/test/strings/basic.jl +++ b/test/strings/basic.jl @@ -10,7 +10,7 @@ using Random # Check that resizing empty source vector does not corrupt string b = IOBuffer() - write(b, "ab") + @inferred write(b, "ab") x = take!(b) s = String(x) resize!(x, 0) @@ -263,8 +263,10 @@ end for c in x nb += write(f, c) end - @test nb == 3 + @test nb === 3 @test String(take!(f)) == "123" + + @test all(T -> T <: Union{Union{}, Int}, Base.return_types(write, (IO, AbstractString))) end @testset "issue #7248" begin