• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37594

pending completion
#37594

push

local

web-flow
Move `round(T::Type, x)` docstring above `round(z::Complex, ...)` docstring (#50775)

73676 of 84540 relevant lines covered (87.15%)

32579691.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.66
/stdlib/REPL/src/LineEdit.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
module LineEdit
4

5
import ..REPL
6
using REPL: AbstractREPL, Options
7

8
using ..Terminals
9
import ..Terminals: raw!, width, height, cmove, getX,
10
                       getY, clear_line, beep
11

12
import Base: ensureroom, show, AnyDict, position
13
using Base: something
14

15
using InteractiveUtils: InteractiveUtils
16

17
abstract type TextInterface end                # see interface immediately below
18
abstract type ModeState end                    # see interface below
19
abstract type HistoryProvider end
20
abstract type CompletionProvider end
21

22
export run_interface, Prompt, ModalInterface, transition, reset_state, edit_insert, keymap
23

24
@nospecialize # use only declared type signatures
25

26
const StringLike = Union{Char,String,SubString{String}}
27

28
# interface for TextInterface
29
function Base.getproperty(ti::TextInterface, name::Symbol)
754✔
30
    if name === :hp
820✔
31
        return getfield(ti, :hp)::HistoryProvider
339✔
32
    elseif name === :complete
813✔
33
        return getfield(ti, :complete)::CompletionProvider
4✔
34
    elseif name === :keymap_dict
813✔
35
        return getfield(ti, :keymap_dict)::Dict{Char,Any}
2,391✔
36
    end
37
    return getfield(ti, name)
26,877✔
38
end
39

40
struct ModalInterface <: TextInterface
41
    modes::Vector{TextInterface}
33✔
42
end
43

44
mutable struct Prompt <: TextInterface
45
    # A string or function to be printed as the prompt.
46
    prompt::Union{String,Function}
77✔
47
    # A string or function to be printed before the prompt. May not change the length of the prompt.
48
    # This may be used for changing the color, issuing other terminal escape codes, etc.
49
    prompt_prefix::Union{String,Function}
50
    # Same as prefix except after the prompt
51
    prompt_suffix::Union{String,Function}
52
    output_prefix::Union{String,Function}
53
    output_prefix_prefix::Union{String,Function}
54
    output_prefix_suffix::Union{String,Function}
55
    keymap_dict::Dict{Char,Any}
56
    repl::Union{AbstractREPL,Nothing}
57
    complete::CompletionProvider
58
    on_enter::Function
59
    on_done::Function
60
    hist::HistoryProvider  # TODO?: rename this `hp` (consistency with other TextInterfaces), or is the type-assert useful for mode(s)?
61
    sticky::Bool
62
end
63

64
show(io::IO, x::Prompt) = show(io, string("Prompt(\"", prompt_string(x.prompt), "\",...)"))
×
65

66

67
mutable struct MIState
68
    interface::ModalInterface
33✔
69
    active_module::Module
70
    current_mode::TextInterface
71
    aborted::Bool
72
    mode_state::IdDict{TextInterface,ModeState}
73
    kill_ring::Vector{String}
74
    kill_idx::Int
75
    previous_key::Vector{Char}
76
    key_repeats::Int
77
    last_action::Symbol
78
    current_action::Symbol
79
end
80

81
MIState(i, mod, c, a, m) = MIState(i, mod, c, a, m, String[], 0, Char[], 0, :none, :none)
33✔
82

83
const BufferLike = Union{MIState,ModeState,IOBuffer}
84
const State = Union{MIState,ModeState}
85

86
function show(io::IO, s::MIState)
×
87
    print(io, "MI State (", mode(s), " active)")
×
88
end
89

90
struct InputAreaState
91
    num_rows::Int64
1,373✔
92
    curs_row::Int64
93
end
94

95
mutable struct PromptState <: ModeState
96
    terminal::AbstractTerminal
80✔
97
    p::Prompt
98
    input_buffer::IOBuffer
99
    region_active::Symbol # :shift or :mark or :off
100
    undo_buffers::Vector{IOBuffer}
101
    undo_idx::Int
102
    ias::InputAreaState
103
    # indentation of lines which do not include the prompt
104
    # if negative, the width of the prompt is used
105
    indent::Int
106
    refresh_lock::Threads.SpinLock
107
    # this would better be Threads.Atomic{Float64}, but not supported on some platforms
108
    beeping::Float64
109
    # this option is to detect when code is pasted in non-"bracketed paste mode" :
110
    last_newline::Float64 # register when last newline was entered
111
    # this option is to speed up output
112
    refresh_wait::Union{Timer,Nothing}
113
end
114

115
struct Modifiers
116
    shift::Bool
117
end
118
Modifiers() = Modifiers(false)
×
119

120
options(s::PromptState) =
8,519✔
121
    if isdefined(s.p, :repl) && isdefined(s.p.repl, :options)
122
        # we can't test isa(s.p.repl, LineEditREPL) as LineEditREPL is defined
123
        # in the REPL module
124
        s.p.repl.options::Options
16,756✔
125
    else
126
        REPL.GlobalOptions::Options
141✔
127
    end
128

129
function setmark(s::MIState, guess_region_active::Bool=true)
28✔
130
    refresh = set_action!(s, :setmark)
28✔
131
    s.current_action === :setmark && s.key_repeats > 0 && activate_region(s, :mark)
14✔
132
    mark(buffer(s))
14✔
133
    refresh && refresh_line(s)
14✔
134
    nothing
14✔
135
end
136

137
# the default mark is 0
138
getmark(s::BufferLike) = max(0, buffer(s).mark)
1,507✔
139

140
const Region = Pair{Int,Int}
141

142
_region(s::BufferLike) = getmark(s) => position(s)
1,390✔
143
region(s::BufferLike) = Pair(extrema(_region(s))...)
2,776✔
144

145
bufend(s::BufferLike) = buffer(s).size
410✔
146

147
axes(reg::Region) = first(reg)+1:last(reg)
468✔
148

149
content(s::BufferLike, reg::Region = 0=>bufend(s)) = String(buffer(s).data[axes(reg)])
834✔
150

151
function activate_region(s::PromptState, state::Symbol)
2,305✔
152
    @assert state in (:mark, :shift, :off)
2,305✔
153
    s.region_active = state
2,743✔
154
    nothing
2,305✔
155
end
156

157
activate_region(s::ModeState, state::Symbol) = false
×
158
deactivate_region(s::ModeState) = activate_region(s, :off)
2,742✔
159

160
is_region_active(s::PromptState) = s.region_active in (:shift, :mark)
1,274✔
161
is_region_active(s::ModeState) = false
×
162

163
region_active(s::PromptState) = s.region_active
2,326✔
164
region_active(s::ModeState) = :off
×
165

166

167
input_string(s::PromptState) = String(take!(copy(s.input_buffer)))
49✔
168

169
input_string_newlines(s::PromptState) = count(c->(c == '\n'), input_string(s))
3✔
170
function input_string_newlines_aftercursor(s::PromptState)
3✔
171
    str = input_string(s)
3✔
172
    isempty(str) && return 0
3✔
173
    rest = str[nextind(str, position(s)):end]
×
174
    return count(c->(c == '\n'), rest)
×
175
end
176

177
struct EmptyCompletionProvider <: CompletionProvider end
178
struct EmptyHistoryProvider <: HistoryProvider end
179

180
reset_state(::EmptyHistoryProvider) = nothing
×
181

182
complete_line(c::EmptyCompletionProvider, s) = String[], "", true
1✔
183

184
# complete_line can be specialized for only two arguments, when the active module
185
# doesn't matter (e.g. Pkg does this)
186
complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s)
1✔
187

188
terminal(s::IO) = s
×
189
terminal(s::PromptState) = s.terminal
6,093✔
190

191

192
function beep(s::PromptState, duration::Real=options(s).beep_duration,
23✔
193
              blink::Real=options(s).beep_blink,
194
              maxduration::Real=options(s).beep_maxduration;
195
              colors=options(s).beep_colors,
196
              use_current::Bool=options(s).beep_use_current)
197
    isinteractive() || return # some tests fail on some platforms
14✔
198
    s.beeping = min(s.beeping + duration, maxduration)
×
199
    let colors = Base.copymutable(colors)
×
200
        errormonitor(@async begin
×
201
            trylock(s.refresh_lock) || return
202
            try
203
                orig_prefix = s.p.prompt_prefix
204
                use_current && push!(colors, prompt_string(orig_prefix))
205
                i = 0
206
                while s.beeping > 0.0
207
                    prefix = colors[mod1(i+=1, end)]
208
                    s.p.prompt_prefix = prefix
209
                    refresh_multi_line(s, beeping=true)
210
                    sleep(blink)
211
                    s.beeping -= blink
212
                end
213
                s.p.prompt_prefix = orig_prefix
214
                refresh_multi_line(s, beeping=true)
215
                s.beeping = 0.0
216
            finally
217
                unlock(s.refresh_lock)
218
            end
219
        end)
220
    end
221
    nothing
×
222
end
223

224
function cancel_beep(s::PromptState)
1,625✔
225
    # wait till beeping finishes
226
    while !trylock(s.refresh_lock)
1,625✔
227
        s.beeping = 0.0
×
228
        sleep(.05)
×
229
    end
×
230
    unlock(s.refresh_lock)
3,250✔
231
    nothing
1,625✔
232
end
233

234
beep(::ModeState) = nothing
2✔
235
cancel_beep(::ModeState) = nothing
×
236

237
for f in Union{Symbol,Expr}[
238
          :terminal, :on_enter, :add_history, :_buffer, :(Base.isempty),
239
          :replace_line, :refresh_multi_line, :input_string, :update_display_buffer,
240
          :empty_undo, :push_undo, :pop_undo, :options, :cancel_beep, :beep,
241
          :deactivate_region, :activate_region, :is_region_active, :region_active]
242
    @eval ($f)(s::MIState, args...) = $(f)(state(s), args...)
9,729✔
243
end
244

245
for f in [:edit_insert, :edit_insert_newline, :edit_backspace, :edit_move_left,
246
          :edit_move_right, :edit_move_word_left, :edit_move_word_right]
247
    @eval function ($f)(s::MIState, args...)
2,295✔
248
        set_action!(s, $(Expr(:quote, f)))
2,295✔
249
        $(f)(state(s), args...)
2,295✔
250
    end
251
end
252

253
const COMMAND_GROUPS =
254
    Dict(:movement    => [:edit_move_left, :edit_move_right, :edit_move_word_left, :edit_move_word_right,
255
                          :edit_move_up, :edit_move_down, :edit_exchange_point_and_mark],
256
         :deletion    => [:edit_clear, :edit_backspace, :edit_delete, :edit_werase,
257
                          :edit_delete_prev_word,
258
                          :edit_delete_next_word,
259
                          :edit_kill_line_forwards, :edit_kill_line_backwards, :edit_kill_region],
260
         :insertion   => [:edit_insert, :edit_insert_newline, :edit_yank],
261
         :replacement => [:edit_yank_pop, :edit_transpose_chars, :edit_transpose_words,
262
                          :edit_upper_case, :edit_lower_case, :edit_title_case, :edit_indent,
263
                          :edit_transpose_lines_up!, :edit_transpose_lines_down!],
264
         :copy        => [:edit_copy_region],
265
         :misc        => [:complete_line, :setmark, :edit_undo!, :edit_redo!])
266

267
const COMMAND_GROUP = Dict{Symbol,Symbol}(command=>group for (group, commands) in COMMAND_GROUPS for command in commands)
268
command_group(command::Symbol) = get(COMMAND_GROUP, command, :nogroup)
4,594✔
269
command_group(command::Function) = command_group(nameof(command))
4✔
270

271
# return true if command should keep active a region
272
function preserve_active(command::Symbol)
2,305✔
273
    command ∈ [:edit_indent, :edit_transpose_lines_down!, :edit_transpose_lines_up!]
2,305✔
274
end
275

276
# returns whether the "active region" status changed visibly,
277
# i.e. whether there should be a visual refresh
278
function set_action!(s::MIState, command::Symbol)
2,513✔
279
    # if a command is already running, don't update the current_action field,
280
    # as the caller is used as a helper function
281
    s.current_action === :unknown || return false
2,720✔
282

283
    active = region_active(s)
2,306✔
284

285
    ## record current action
286
    s.current_action = command
2,306✔
287

288
    ## handle activeness of the region
289
    if startswith(String(command), "shift_") # shift-move command
4,612✔
290
        if active !== :shift
1✔
291
            setmark(s) # s.current_action must already have been set
1✔
292
            activate_region(s, :shift)
1✔
293
            # NOTE: if the region was already active from a non-shift
294
            # move (e.g. ^Space^Space), the region is visibly changed
295
            return active !== :off # active status is reset
1✔
296
        end
297
    elseif !(preserve_active(command) ||
4,609✔
298
             command_group(command) === :movement && region_active(s) === :mark)
299
        # if we move after a shift-move, the region is de-activated
300
        # (e.g. like emacs behavior)
301
        deactivate_region(s)
2,304✔
302
        return active !== :off
2,304✔
303
    end
304
    false
1✔
305
end
306

307
set_action!(s, command::Symbol) = nothing
3✔
308

309
function common_prefix(completions::Vector{String})
1✔
310
    ret = ""
×
311
    c1 = completions[1]
1✔
312
    isempty(c1) && return ret
1✔
313
    i = 1
×
314
    cc, nexti = iterate(c1, i)
2✔
315
    while true
6✔
316
        for c in completions
6✔
317
            (i > lastindex(c) || c[i] != cc) && return ret
24✔
318
        end
16✔
319
        ret = string(ret, cc)
5✔
320
        i >= lastindex(c1) && return ret
5✔
321
        i = nexti
×
322
        cc, nexti = iterate(c1, i)
10✔
323
    end
5✔
324
end
325

326
# This is the maximum number of completions that will be displayed in a single
327
# column, anything above that and multiple columns will be used. Note that this
328
# does not restrict column length when multiple columns are used.
329
const MULTICOLUMN_THRESHOLD = 5
330

331
# Show available completions
332
function show_completions(s::PromptState, completions::Vector{String})
3✔
333
    # skip any lines of input after the cursor
334
    cmove_down(terminal(s), input_string_newlines_aftercursor(s))
3✔
335
    println(terminal(s))
3✔
336
    if any(Base.Fix1(occursin, '\n'), completions)
13✔
337
        foreach(Base.Fix1(println, terminal(s)), completions)
1✔
338
    else
339
        n = length(completions)
2✔
340
        colmax = 2 + maximum(length, completions; init=1) # n.b. length >= textwidth
2✔
341

342
        num_cols = min(cld(n, MULTICOLUMN_THRESHOLD),
2✔
343
                       max(div(width(terminal(s)), colmax), 1))
344

345
        entries_per_col = cld(n, num_cols)
2✔
346
        idx = 0
×
347
        for _ in 1:entries_per_col
4✔
348
            for col = 0:(num_cols-1)
12✔
349
                idx += 1
9✔
350
                idx > n && break
9✔
351
                cmove_col(terminal(s), colmax*col+1)
9✔
352
                print(terminal(s), completions[idx])
9✔
353
            end
9✔
354
            println(terminal(s))
6✔
355
        end
6✔
356
    end
357

358
    # make space for the prompt
359
    for i = 1:input_string_newlines(s)
3✔
360
        println(terminal(s))
×
361
    end
×
362
end
363

364
# Prompt Completions
365
function complete_line(s::MIState)
4✔
366
    set_action!(s, :complete_line)
4✔
367
    if complete_line(state(s), s.key_repeats, s.active_module)
4✔
368
        return refresh_line(s)
3✔
369
    else
370
        beep(s)
1✔
371
        return :ignore
1✔
372
    end
373
end
374

375
function complete_line(s::PromptState, repeats::Int, mod::Module)
4✔
376
    completions, partial, should_complete = complete_line(s.p.complete, s, mod)::Tuple{Vector{String},String,Bool}
4✔
377
    isempty(completions) && return false
4✔
378
    if !should_complete
3✔
379
        # should_complete is false for cases where we only want to show
380
        # a list of possible completions but not complete, e.g. foo(\t
381
        show_completions(s, completions)
×
382
    elseif length(completions) == 1
3✔
383
        # Replace word by completion
384
        prev_pos = position(s)
2✔
385
        push_undo(s)
2✔
386
        edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1])
2✔
387
    else
388
        p = common_prefix(completions)
1✔
389
        if !isempty(p) && p != partial
1✔
390
            # All possible completions share the same prefix, so we might as
391
            # well complete that
392
            prev_pos = position(s)
1✔
393
            push_undo(s)
1✔
394
            edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, p)
1✔
395
        elseif repeats > 0
×
396
            show_completions(s, completions)
×
397
        end
398
    end
399
    return true
3✔
400
end
401

402
function clear_input_area(terminal::AbstractTerminal, s::PromptState)
231✔
403
    if s.refresh_wait !== nothing
231✔
404
        close(s.refresh_wait)
×
405
        s.refresh_wait = nothing
×
406
    end
407
    _clear_input_area(terminal, s.ias)
231✔
408
    s.ias = InputAreaState(0, 0)
231✔
409
end
410
clear_input_area(terminal::AbstractTerminal, s::ModeState) = (_clear_input_area(terminal, s.ias); s.ias = InputAreaState(0, 0))
106✔
411
clear_input_area(s::ModeState) = clear_input_area(s.terminal, s)
×
412

413
function _clear_input_area(terminal::AbstractTerminal, state::InputAreaState)
1,655✔
414
    # Go to the last line
415
    if state.curs_row < state.num_rows
1,655✔
416
        cmove_down(terminal, state.num_rows - state.curs_row)
38✔
417
    end
418

419
    # Clear lines one by one going up
420
    for j = 2:state.num_rows
2,260✔
421
        clear_line(terminal)
1,227✔
422
        cmove_up(terminal)
1,227✔
423
    end
1,849✔
424

425
    # Clear top line
426
    clear_line(terminal)
1,655✔
427
    nothing
1,655✔
428
end
429

430
prompt_string(s::PromptState) = prompt_string(s.p)
×
431
prompt_string(p::Prompt) = prompt_string(p.prompt)
×
432
prompt_string(s::AbstractString) = s
×
433
prompt_string(f::Function) = Base.invokelatest(f)
2,345✔
434

435
function refresh_multi_line(s::PromptState; kw...)
1,796✔
436
    if s.refresh_wait !== nothing
898✔
437
        close(s.refresh_wait)
×
438
        s.refresh_wait = nothing
×
439
    end
440
    refresh_multi_line(terminal(s), s; kw...)
898✔
441
end
442
refresh_multi_line(s::ModeState; kw...) = refresh_multi_line(terminal(s), s; kw...)
124✔
443
refresh_multi_line(termbuf::TerminalBuffer, s::ModeState; kw...) = refresh_multi_line(termbuf, terminal(s), s; kw...)
532✔
444
refresh_multi_line(termbuf::TerminalBuffer, term, s::ModeState; kw...) = (@assert term === terminal(s); refresh_multi_line(termbuf,s; kw...))
84✔
445

446
function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf::IOBuffer,
2,742✔
447
                            state::InputAreaState, prompt = "";
448
                            indent::Int = 0, region_active::Bool = false)
449
    _clear_input_area(termbuf, state)
1,371✔
450

451
    cols = width(terminal)
1,371✔
452
    rows = height(terminal)
1,371✔
453
    curs_row = -1 # relative to prompt (1-based)
×
454
    curs_pos = -1 # 1-based column position of the cursor
×
455
    cur_row = 0   # count of the number of rows
×
456
    buf_pos = position(buf)
1,371✔
457
    line_pos = buf_pos
×
458
    regstart, regstop = region(buf)
2,742✔
459
    written = 0
×
460
    # Write out the prompt string
461
    lindent = write_prompt(termbuf, prompt, hascolor(terminal))::Int
1,371✔
462
    # Count the '\n' at the end of the line if the terminal emulator does (specific to DOS cmd prompt)
463
    miscountnl = @static Sys.iswindows() ? (isa(Terminals.pipe_reader(terminal), Base.TTY) && !(Base.ispty(Terminals.pipe_reader(terminal)))::Bool) : false
×
464

465
    # Now go through the buffer line by line
466
    seek(buf, 0)
1,371✔
467
    moreinput = true # add a blank line if there is a trailing newline on the last line
×
468
    lastline = false # indicates when to stop printing lines, even when there are potentially
×
469
                     # more (for the case where rows is too small to print everything)
470
                     # Note: when there are too many lines for rows, we still print the first lines
471
                     # even if they are going to not be visible in the end: for simplicity, but
472
                     # also because it does the 'right thing' when the window is resized
473
    while moreinput
2,997✔
474
        line = readline(buf, keep=true)
1,626✔
475
        moreinput = endswith(line, "\n")
1,626✔
476
        if rows == 1 && line_pos <= sizeof(line) - moreinput
1,626✔
477
            # we special case rows == 1, as otherwise by the time the cursor is seen to
478
            # be in the current line, it's too late to chop the '\n' away
479
            lastline = true
×
480
            curs_row = 1
×
481
            curs_pos = lindent + line_pos
×
482
        end
483
        if moreinput && lastline # we want to print only one "visual" line, so
1,626✔
484
            line = chomp(line)   # don't include the trailing "\n"
×
485
        end
486
        # We need to deal with on-screen characters, so use textwidth to compute occupied columns
487
        llength = textwidth(line)
3,252✔
488
        slength = sizeof(line)
3,252✔
489
        cur_row += 1
1,626✔
490
        # lwrite: what will be written to termbuf
491
        lwrite = region_active ? highlight_region(line, regstart, regstop, written, slength) :
1,630✔
492
                                 line
493
        written += slength
1,626✔
494
        cmove_col(termbuf, lindent + 1)
1,626✔
495
        write(termbuf, lwrite)
1,626✔
496
        # We expect to be line after the last valid output line (due to
497
        # the '\n' at the end of the previous line)
498
        if curs_row == -1
1,626✔
499
            line_pos -= slength # '\n' gets an extra pos
1,584✔
500
            # in this case, we haven't yet written the cursor position
501
            if line_pos < 0 || !moreinput
2,955✔
502
                num_chars = line_pos >= 0 ?
1,584✔
503
                                llength :
504
                                textwidth(line[1:prevind(line, line_pos + slength + 1)])
505
                curs_row, curs_pos = divrem(lindent + num_chars - 1, cols)
1,371✔
506
                curs_row += cur_row
1,371✔
507
                curs_pos += 1
1,371✔
508
                # There's an issue if the cursor is after the very right end of the screen. In that case we need to
509
                # move the cursor to the next line, and emit a newline if needed
510
                if curs_pos == cols
1,371✔
511
                    # only emit the newline if the cursor is at the end of the line we're writing
512
                    if line_pos == 0
8✔
513
                        write(termbuf, "\n")
8✔
514
                        cur_row += 1
8✔
515
                    end
516
                    curs_row += 1
8✔
517
                    curs_pos = 0
×
518
                    cmove_col(termbuf, 1)
8✔
519
                end
520
            end
521
        end
522
        cur_row += div(max(lindent + llength + miscountnl - 1, 0), cols)
1,626✔
523
        lindent = indent < 0 ? lindent : indent
1,626✔
524

525
        lastline && break
1,626✔
526
        if curs_row >= 0 && cur_row + 1 >= rows &&             # when too many lines,
1,626✔
527
                            cur_row - curs_row + 1 >= rows ÷ 2 # center the cursor
528
            lastline = true
×
529
        end
530
    end
1,626✔
531
    seek(buf, buf_pos)
1,371✔
532

533
    # Let's move the cursor to the right position
534
    # The line first
535
    n = cur_row - curs_row
1,371✔
536
    if n > 0
1,371✔
537
        cmove_up(termbuf, n)
40✔
538
    end
539

540
    #columns are 1 based
541
    cmove_col(termbuf, curs_pos + 1)
1,371✔
542
    # Updated cur_row,curs_row
543
    return InputAreaState(cur_row, curs_row)
1,371✔
544
end
545

546
function highlight_region(lwrite::Union{String,SubString{String}}, regstart::Int, regstop::Int, written::Int, slength::Int)
4✔
547
    if written <= regstop <= written+slength
4✔
548
        i = thisind(lwrite, regstop-written)
8✔
549
        lwrite = lwrite[1:i] * Base.disable_text_style[:reverse] * lwrite[nextind(lwrite, i):end]
8✔
550
    end
551
    if written <= regstart <= written+slength
4✔
552
        i = thisind(lwrite, regstart-written)
8✔
553
        lwrite = lwrite[1:i] * Base.text_colors[:reverse] * lwrite[nextind(lwrite, i):end]
8✔
554
    end
555
    return lwrite
4✔
556
end
557

558
function refresh_multi_line(terminal::UnixTerminal, args...; kwargs...)
2,168✔
559
    outbuf = IOBuffer()
1,084✔
560
    termbuf = TerminalBuffer(outbuf)
1,084✔
561
    ret = refresh_multi_line(termbuf, terminal, args...;kwargs...)
1,084✔
562
    # Output the entire refresh at once
563
    write(terminal, take!(outbuf))
1,084✔
564
    flush(terminal)
×
565
    return ret
1,084✔
566
end
567

568

569
# Edit functionality
570
is_non_word_char(c::Char) = c in """ \t\n\"\\'`@\$><=:;|&{}()[].,+-*/?%^~"""
465✔
571

572
function reset_key_repeats(f::Function, s::MIState)
24✔
573
    key_repeats_sav = s.key_repeats
24✔
574
    try
24✔
575
        s.key_repeats = 0
24✔
576
        return f()
24✔
577
    finally
578
        s.key_repeats = key_repeats_sav
24✔
579
    end
580
end
581

582
function edit_exchange_point_and_mark(s::MIState)
5✔
583
    set_action!(s, :edit_exchange_point_and_mark)
5✔
584
    return edit_exchange_point_and_mark(buffer(s)) ? refresh_line(s) : false
5✔
585
end
586

587
function edit_exchange_point_and_mark(buf::IOBuffer)
7✔
588
    m = getmark(buf)
7✔
589
    m == position(buf) && return false
7✔
590
    mark(buf)
7✔
591
    seek(buf, m)
7✔
592
    return true
7✔
593
end
594

595
char_move_left(s::PromptState) = char_move_left(s.input_buffer)
×
596
function char_move_left(buf::IOBuffer)
377✔
597
    while position(buf) > 0
411✔
598
        seek(buf, position(buf)-1)
411✔
599
        c = peek(buf)
411✔
600
        (((c & 0x80) == 0) || ((c & 0xc0) == 0xc0)) && break
476✔
601
    end
34✔
602
    pos = position(buf)
377✔
603
    c = read(buf, Char)
377✔
604
    seek(buf, pos)
377✔
605
    return c
377✔
606
end
607

608
function edit_move_left(buf::IOBuffer)
55✔
609
    if position(buf) > 0
55✔
610
        #move to the next base UTF8 character to the left
611
        while true
57✔
612
            c = char_move_left(buf)
57✔
613
            if textwidth(c) != 0 || c == '\n' || position(buf) == 0
63✔
614
                break
55✔
615
            end
616
        end
2✔
617
        return true
55✔
618
    end
619
    return false
×
620
end
621

622
edit_move_left(s::PromptState) = edit_move_left(s.input_buffer) ? refresh_line(s) : false
47✔
623

624
function edit_move_word_left(s::PromptState)
4✔
625
    if position(s) > 0
4✔
626
        char_move_word_left(s.input_buffer)
4✔
627
        return refresh_line(s)
4✔
628
    end
629
    return nothing
×
630
end
631

632
char_move_right(s::MIState) = char_move_right(buffer(s))
×
633
function char_move_right(buf::IOBuffer)
211✔
634
    return !eof(buf) && read(buf, Char)
211✔
635
end
636

637
function char_move_word_right(buf::IOBuffer, is_delimiter::Function=is_non_word_char)
128✔
638
    while !eof(buf) && is_delimiter(char_move_right(buf))
140✔
639
    end
12✔
640
    while !eof(buf)
138✔
641
        pos = position(buf)
124✔
642
        if is_delimiter(char_move_right(buf))
124✔
643
            seek(buf, pos)
50✔
644
            break
50✔
645
        end
646
    end
138✔
647
end
648

649
function char_move_word_left(buf::IOBuffer, is_delimiter::Function=is_non_word_char)
137✔
650
    while position(buf) > 0 && is_delimiter(char_move_left(buf))
202✔
651
    end
65✔
652
    while position(buf) > 0
157✔
653
        pos = position(buf)
141✔
654
        if is_delimiter(char_move_left(buf))
141✔
655
            seek(buf, pos)
54✔
656
            break
54✔
657
        end
658
    end
157✔
659
end
660

661
char_move_word_right(s::Union{MIState,ModeState}) = char_move_word_right(buffer(s))
1✔
662
char_move_word_left(s::Union{MIState,ModeState}) = char_move_word_left(buffer(s))
×
663

664
function edit_move_right(buf::IOBuffer)
13✔
665
    if !eof(buf)
13✔
666
        # move to the next base UTF8 character to the right
667
        while true
13✔
668
            c = char_move_right(buf)
13✔
669
            eof(buf) && break
13✔
670
            pos = position(buf)
10✔
671
            nextc = read(buf,Char)
10✔
672
            seek(buf,pos)
10✔
673
            (textwidth(nextc) != 0 || nextc == '\n') && break
10✔
674
        end
1✔
675
        return true
12✔
676
    end
677
    return false
1✔
678
end
679
edit_move_right(s::PromptState) = edit_move_right(s.input_buffer) ? refresh_line(s) : false
2✔
680

681
function edit_move_word_right(s::PromptState)
1✔
682
    if !eof(s.input_buffer)
1✔
683
        char_move_word_right(s)
1✔
684
        return refresh_line(s)
1✔
685
    end
686
    return nothing
×
687
end
688

689
## Move line up/down
690
# Querying the terminal is expensive, memory access is cheap
691
# so to find the current column, we find the offset for the start
692
# of the line.
693

694
function edit_move_up(buf::IOBuffer)
37✔
695
    npos = findprev(isequal(UInt8('\n')), buf.data, position(buf))
54✔
696
    npos === nothing && return false # we're in the first line
37✔
697
    # We're interested in character count, not byte count
698
    offset = length(content(buf, npos => position(buf)))
17✔
699
    npos2 = something(findprev(isequal(UInt8('\n')), buf.data, npos-1), 0)
22✔
700
    seek(buf, npos2)
17✔
701
    for _ = 1:offset
30✔
702
        pos = position(buf)
55✔
703
        if read(buf, Char) == '\n'
55✔
704
            seek(buf, pos)
4✔
705
            break
4✔
706
        end
707
    end
51✔
708
    return true
17✔
709
end
710
function edit_move_up(s::MIState)
4✔
711
    set_action!(s, :edit_move_up)
12✔
712
    changed = edit_move_up(buffer(s))
12✔
713
    changed && refresh_line(s)
12✔
714
    return changed
12✔
715
end
716

717
function edit_move_down(buf::IOBuffer)
30✔
718
    npos = something(findprev(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)), 0)
47✔
719
    # We're interested in character count, not byte count
720
    offset = length(String(buf.data[(npos+1):(position(buf))]))
30✔
721
    npos2 = findnext(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)+1)
54✔
722
    if npos2 === nothing #we're in the last line
30✔
723
        return false
6✔
724
    end
725
    seek(buf, npos2)
24✔
726
    for _ = 1:offset
44✔
727
        pos = position(buf)
87✔
728
        if eof(buf) || read(buf, Char) == '\n'
163✔
729
            seek(buf, pos)
11✔
730
            break
11✔
731
        end
732
    end
76✔
733
    return true
24✔
734
end
735
function edit_move_down(s::MIState)
5✔
736
    set_action!(s, :edit_move_down)
5✔
737
    changed = edit_move_down(buffer(s))
5✔
738
    changed && refresh_line(s)
5✔
739
    return changed
5✔
740
end
741

742
function edit_shift_move(s::MIState, move_function::Function)
4✔
743
    @assert command_group(move_function) === :movement
4✔
744
    set_action!(s, Symbol(:shift_, move_function))
4✔
745
    return move_function(s)
4✔
746
end
747

748

749
# splice! for IOBuffer: convert from close-open region to index, update the size,
750
# and keep the cursor position and mark stable with the text
751
# returns the removed portion as a String
752
function edit_splice!(s::BufferLike, r::Region=region(s), ins::String = ""; rigid_mark::Bool=true)
457✔
753
    A, B = first(r), last(r)
180✔
754
    A >= B && isempty(ins) && return String(ins)
180✔
755
    buf = buffer(s)
212✔
756
    pos = position(buf)
178✔
757
    adjust_pos = true
×
758
    if A <= pos < B
178✔
759
        seek(buf, A)
65✔
760
    elseif B <= pos
113✔
761
        seek(buf, pos - B + A)
93✔
762
    else
763
        adjust_pos = false
×
764
    end
765
    if A < buf.mark  < B || A == buf.mark == B
354✔
766
        # rigid_mark is used only if the mark is strictly "inside"
767
        # the region, or the region is empty and the mark is at the boundary
768
        buf.mark = rigid_mark ? A : A + sizeof(ins)
8✔
769
    elseif buf.mark >= B
171✔
770
        buf.mark += sizeof(ins) - B + A
7✔
771
    end
772
    ensureroom(buf, B) # handle !buf.reinit from take!
356✔
773
    ret = splice!(buf.data, A+1:B, codeunits(String(ins))) # position(), etc, are 0-indexed
221✔
774
    buf.size = buf.size + sizeof(ins) - B + A
178✔
775
    adjust_pos && seek(buf, position(buf) + sizeof(ins))
178✔
776
    return String(copy(ret))
178✔
777
end
778

779
edit_splice!(s::MIState, ins::AbstractString) = edit_splice!(s, region(s), ins)
3✔
780

781
function edit_insert(s::PromptState, c::StringLike)
2,194✔
782
    push_undo(s)
2,197✔
783
    buf = s.input_buffer
2,194✔
784

785
    if ! options(s).auto_indent_bracketed_paste
4,363✔
786
        pos = position(buf)
1,275✔
787
        if pos > 0
1,275✔
788
            if buf.data[pos] != _space && string(c) != " "
2,261✔
789
                options(s).auto_indent_tmp_off = false
1,997✔
790
            end
791
            if buf.data[pos] == _space
1,204✔
792
                #tabulators are already expanded to space
793
                #this expansion may take longer than auto_indent_time_threshold which breaks the timing
794
                s.last_newline = time()
109✔
795
            else
796
                #if characters after new line are coming in very fast
797
                #its probably copy&paste => switch auto-indent off for the next coming new line
798
                if ! options(s).auto_indent_tmp_off && time() - s.last_newline < options(s).auto_indent_time_threshold
2,114✔
799
                    options(s).auto_indent_tmp_off = true
833✔
800
                end
801
            end
802
        end
803
    end
804

805
    old_wait = s.refresh_wait !== nothing
2,194✔
806
    if old_wait
2,194✔
807
        close(s.refresh_wait)
×
808
        s.refresh_wait = nothing
×
809
    end
810
    str = string(c)
4,342✔
811
    edit_insert(buf, str)
2,209✔
812
    if '\n' in str
2,194✔
813
        refresh_line(s)
11✔
814
    else
815
        after = options(s).auto_refresh_time_delay
4,352✔
816
        termbuf = terminal(s)
2,183✔
817
        w = width(termbuf)
2,183✔
818
        offset = s.ias.curs_row == 1 || s.indent < 0 ?
2,707✔
819
            sizeof(prompt_string(s.p.prompt)::String) : s.indent
820
        offset += position(buf) - beginofline(buf) # size of current line
2,227✔
821
        spinner = '\0'
×
822
        delayup = !eof(buf) || old_wait
2,183✔
823
        if offset + textwidth(str) <= w && !(after == 0 && delayup)
2,183✔
824
            # Avoid full update when appending characters to the end
825
            # and an update of curs_row isn't necessary (conservatively estimated)
826
            write(termbuf, str)
1,689✔
827
            spinner = ' ' # temporarily clear under the cursor
1,689✔
828
        elseif after == 0
494✔
829
            refresh_line(s)
494✔
830
            delayup = false
494✔
831
        else # render a spinner for each key press
832
            if old_wait || length(str) != 1
×
833
                spinner = spin_seq[mod1(position(buf) - w, length(spin_seq))]
×
834
            else
835
                spinner = str[end]
×
836
            end
837
            delayup = true
×
838
        end
839
        if delayup
2,183✔
840
            if spinner != '\0'
×
841
                write(termbuf, spinner)
×
842
                cmove_left(termbuf)
×
843
            end
844
            s.refresh_wait = Timer(after) do t
×
845
                s.refresh_wait === t || return
846
                s.refresh_wait = nothing
847
                refresh_line(s)
848
            end
849
        end
850
    end
851
    nothing
2,194✔
852
end
853
const spin_seq = ("⋯", "⋱", "⋮", "⋰")
854

855
function edit_insert(buf::IOBuffer, c::StringLike)
25✔
856
    if eof(buf)
2,244✔
857
        return write(buf, c)
2,218✔
858
    else
859
        s = string(c)
×
860
        edit_splice!(buf, position(buf) => position(buf), s)
26✔
861
        return sizeof(s)
26✔
862
    end
863
end
864

865
# align: number of ' ' to insert after '\n'
866
# if align < 0: align like line above
867
function edit_insert_newline(s::PromptState, align::Int = 0 - options(s).auto_indent)
15✔
868
    push_undo(s)
33✔
869
    buf = buffer(s)
15✔
870
    autoindent = align < 0
15✔
871
    if autoindent && ! options(s).auto_indent_tmp_off
19✔
872
        beg = beginofline(buf)
18✔
873
        align = min(something(findnext(_notspace, buf.data[beg+1:buf.size], 1), 0) - 1,
19✔
874
                    position(buf) - beg) # indentation must not increase
875
        align < 0 && (align = buf.size-beg)
10✔
876
    #else
877
    #    align = 0
878
    end
879
    align < 0 && (align = 0)
15✔
880
    edit_insert(buf, '\n' * ' '^align)
20✔
881
    refresh_line(s)
15✔
882
    # updating s.last_newline should happen after refresh_line(s) which can take
883
    # an unpredictable amount of time and makes "paste detection" unreliable
884
    if ! options(s).auto_indent_bracketed_paste
19✔
885
        s.last_newline = time()
15✔
886
    end
887
    nothing
15✔
888
end
889

890
# align: delete up to 4 spaces to align to a multiple of 4 chars
891
# adjust: also delete spaces on the right of the cursor to try to keep aligned what is
892
# on the right
893
function edit_backspace(s::PromptState, align::Bool=options(s).backspace_align,
32✔
894
                        adjust::Bool=options(s).backspace_adjust)
895
    push_undo(s)
77✔
896
    if edit_backspace(buffer(s), align, adjust)
32✔
897
        return refresh_line(s)
32✔
898
    else
899
        pop_undo(s)
×
900
        return beep(s)
×
901
    end
902
end
903

904
const _newline =  UInt8('\n')
905
const _space = UInt8(' ')
906

907
_notspace(c) = c != _space
164✔
908

909
beginofline(buf::IOBuffer, pos::Int=position(buf)) = something(findprev(isequal(_newline), buf.data, pos), 0)
4,607✔
910

911
function endofline(buf::IOBuffer, pos::Int=position(buf))
40✔
912
    eol = findnext(isequal(_newline), buf.data[pos+1:buf.size], 1)
60✔
913
    eol === nothing ? buf.size : pos + eol - 1
50✔
914
end
915

916
function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=false)
33✔
917
    !align && adjust &&
33✔
918
        throw(DomainError((align, adjust),
919
                          "if `adjust` is `true`, `align` must be `true`"))
920
    oldpos = position(buf)
33✔
921
    oldpos == 0 && return false
33✔
922
    c = char_move_left(buf)
33✔
923
    newpos = position(buf)
33✔
924
    if align && c == ' ' # maybe delete multiple spaces
33✔
925
        beg = beginofline(buf, newpos)
12✔
926
        align = textwidth(String(buf.data[1+beg:newpos])) % 4
6✔
927
        nonspace = something(findprev(_notspace, buf.data, newpos), 0)
12✔
928
        if newpos - align >= nonspace
6✔
929
            newpos -= align
6✔
930
            seek(buf, newpos)
6✔
931
            if adjust
6✔
932
                spaces = something(findnext(_notspace, buf.data[newpos+2:buf.size], 1), 0)
4✔
933
                oldpos = spaces == 0 ? buf.size :
4✔
934
                    buf.data[newpos+1+spaces] == _newline ? newpos+spaces :
935
                    newpos + min(spaces, 4)
936
            end
937
        end
938
    end
939
    edit_splice!(buf, newpos => oldpos)
33✔
940
    return true
33✔
941
end
942

943
function edit_delete(s::MIState)
1✔
944
    set_action!(s, :edit_delete)
1✔
945
    push_undo(s)
1✔
946
    if edit_delete(buffer(s))
1✔
947
        return refresh_line(s)
1✔
948
    else
949
        pop_undo(s)
×
950
        return beep(s)
×
951
    end
952
end
953

954
function edit_delete(buf::IOBuffer)
1✔
955
    eof(buf) && return false
1✔
956
    oldpos = position(buf)
1✔
957
    char_move_right(buf)
1✔
958
    edit_splice!(buf, oldpos => position(buf))
1✔
959
    return true
1✔
960
end
961

962
function edit_werase(buf::IOBuffer)
3✔
963
    pos1 = position(buf)
3✔
964
    char_move_word_left(buf, isspace)
3✔
965
    pos0 = position(buf)
3✔
966
    return edit_splice!(buf, pos0 => pos1)
3✔
967
end
968

969
function edit_werase(s::MIState)
3✔
970
    set_action!(s, :edit_werase)
3✔
971
    push_undo(s)
3✔
972
    if push_kill!(s, edit_werase(buffer(s)), rev=true)
3✔
973
        return refresh_line(s)
3✔
974
    else
975
        pop_undo(s)
×
976
        return :ignore
×
977
    end
978
end
979

980
function edit_delete_prev_word(buf::IOBuffer)
8✔
981
    pos1 = position(buf)
8✔
982
    char_move_word_left(buf)
8✔
983
    pos0 = position(buf)
8✔
984
    return edit_splice!(buf, pos0 => pos1)
8✔
985
end
986

987
function edit_delete_prev_word(s::MIState)
3✔
988
    set_action!(s, :edit_delete_prev_word)
3✔
989
    push_undo(s)
3✔
990
    if push_kill!(s, edit_delete_prev_word(buffer(s)), rev=true)
3✔
991
        return refresh_line(s)
3✔
992
    else
993
        pop_undo(s)
×
994
        return :ignore
×
995
    end
996
end
997

998
function edit_delete_next_word(buf::IOBuffer)
3✔
999
    pos0 = position(buf)
3✔
1000
    char_move_word_right(buf)
3✔
1001
    pos1 = position(buf)
3✔
1002
    return edit_splice!(buf, pos0 => pos1)
3✔
1003
end
1004

1005
function edit_delete_next_word(s::MIState)
3✔
1006
    set_action!(s, :edit_delete_next_word)
3✔
1007
    push_undo(s)
3✔
1008
    if push_kill!(s, edit_delete_next_word(buffer(s)))
3✔
1009
        return refresh_line(s)
3✔
1010
    else
1011
        pop_undo(s)
×
1012
        return :ignore
×
1013
    end
1014
end
1015

1016
function edit_yank(s::MIState)
9✔
1017
    set_action!(s, :edit_yank)
9✔
1018
    if isempty(s.kill_ring)
9✔
1019
        beep(s)
×
1020
        return :ignore
×
1021
    end
1022
    setmark(s) # necessary for edit_yank_pop
9✔
1023
    push_undo(s)
9✔
1024
    edit_insert(buffer(s), s.kill_ring[mod1(s.kill_idx, end)])
12✔
1025
    return refresh_line(s)
9✔
1026
end
1027

1028
function edit_yank_pop(s::MIState, require_previous_yank::Bool=true)
7✔
1029
    set_action!(s, :edit_yank_pop)
7✔
1030
    repeat = s.last_action ∈ (:edit_yank, :edit_yank_pop)
4✔
1031
    if require_previous_yank && !repeat || isempty(s.kill_ring)
7✔
1032
        beep(s)
1✔
1033
        return :ignore
1✔
1034
    else
1035
        require_previous_yank || repeat || setmark(s)
4✔
1036
        push_undo(s)
3✔
1037
        edit_splice!(s, s.kill_ring[mod1(s.kill_idx -= 1, end)])
6✔
1038
        return refresh_line(s)
3✔
1039
    end
1040
end
1041

1042
function push_kill!(s::MIState, killed::String, concat::Bool = s.key_repeats > 0; rev::Bool=false)
102✔
1043
    isempty(killed) && return false
39✔
1044
    if concat && !isempty(s.kill_ring)
37✔
1045
        s.kill_ring[end] = rev ?
8✔
1046
            killed * s.kill_ring[end] : # keep expected order for backward deletion
1047
            s.kill_ring[end] * killed
1048
    else
1049
        push!(s.kill_ring, killed)
31✔
1050
        length(s.kill_ring) > options(s).kill_ring_max && popfirst!(s.kill_ring)
31✔
1051
    end
1052
    s.kill_idx = lastindex(s.kill_ring)
37✔
1053
    return true
37✔
1054
end
1055

1056
function edit_kill_line(s::MIState, backwards::Bool=false)
23✔
1057
    buf = buffer(s)
23✔
1058
    if backwards
15✔
1059
        set_action!(s, :edit_kill_line_backwards)
7✔
1060
        pos = beginofline(buf)
10✔
1061
        endpos = position(buf)
7✔
1062
        pos == endpos && pos > 0 && (pos -= 1)
7✔
1063
    else
1064
        set_action!(s, :edit_kill_line_forwards)
8✔
1065
        pos = position(buf)
8✔
1066
        endpos = endofline(buf)
10✔
1067
        endpos == pos && buf.size > pos && (endpos += 1)
8✔
1068
    end
1069
    push_undo(s)
15✔
1070
    if push_kill!(s, edit_splice!(s, pos => endpos); rev=backwards)
15✔
1071
        return refresh_line(s)
14✔
1072
    else
1073
        pop_undo(s)
1✔
1074
        beep(s)
1✔
1075
        return :ignore
1✔
1076
    end
1077
end
1078

1079
edit_kill_line_forwards(s::MIState) = edit_kill_line(s, false)
×
1080
edit_kill_line_backwards(s::MIState) = edit_kill_line(s, true)
7✔
1081

1082
function edit_copy_region(s::MIState)
1✔
1083
    set_action!(s, :edit_copy_region)
1✔
1084
    buf = buffer(s)
1✔
1085
    push_kill!(s, content(buf, region(buf)), false) || return :ignore
1✔
1086
    if options(s).region_animation_duration > 0.0
1✔
1087
        edit_exchange_point_and_mark(s)
1✔
1088
        sleep(options(s).region_animation_duration)
1✔
1089
        edit_exchange_point_and_mark(s)
1✔
1090
    end
1091
    nothing
1✔
1092
end
1093

1094
function edit_kill_region(s::MIState)
2✔
1095
    set_action!(s, :edit_kill_region)
2✔
1096
    push_undo(s)
2✔
1097
    if push_kill!(s, edit_splice!(s), false)
2✔
1098
        return refresh_line(s)
2✔
1099
    else
1100
        pop_undo(s)
×
1101
        return :ignore
×
1102
    end
1103
end
1104

1105
function edit_transpose_chars(s::MIState)
1✔
1106
    set_action!(s, :edit_transpose_chars)
1✔
1107
    push_undo(s)
1✔
1108
    return edit_transpose_chars(buffer(s)) ? refresh_line(s) : pop_undo(s)
1✔
1109
end
1110

1111
function edit_transpose_chars(buf::IOBuffer)
14✔
1112
    # Moving left but not transpoing anything is intentional, and matches Emacs's behavior
1113
    eof(buf) && position(buf) !== 0 && char_move_left(buf)
14✔
1114
    position(buf) == 0 && return false
14✔
1115
    char_move_left(buf)
9✔
1116
    pos = position(buf)
9✔
1117
    a, b = read(buf, Char), read(buf, Char)
9✔
1118
    seek(buf, pos)
9✔
1119
    write(buf, b, a)
9✔
1120
    return true
9✔
1121
end
1122

1123
function edit_transpose_words(s::MIState)
1✔
1124
    set_action!(s, :edit_transpose_words)
1✔
1125
    push_undo(s)
1✔
1126
    return edit_transpose_words(buffer(s)) ? refresh_line(s) : pop_undo(s)
1✔
1127
end
1128

1129
function edit_transpose_words(buf::IOBuffer, mode::Symbol=:emacs)
18✔
1130
    mode in [:readline, :emacs] ||
18✔
1131
        throw(ArgumentError("`mode` must be `:readline` or `:emacs`"))
1132
    pos = position(buf)
17✔
1133
    if mode === :emacs
17✔
1134
        char_move_word_left(buf)
6✔
1135
        char_move_word_right(buf)
6✔
1136
    end
1137
    char_move_word_right(buf)
17✔
1138
    e2 = position(buf)
17✔
1139
    char_move_word_left(buf)
17✔
1140
    b2 = position(buf)
17✔
1141
    char_move_word_left(buf)
17✔
1142
    b1 = position(buf)
17✔
1143
    char_move_word_right(buf)
17✔
1144
    e1 = position(buf)
17✔
1145
    e1 >= b2 && (seek(buf, pos); return false)
20✔
1146
    word2 = edit_splice!(buf, b2 => e2, content(buf, b1 => e1))
14✔
1147
    edit_splice!(buf, b1 => e1, word2)
14✔
1148
    seek(buf, e2)
14✔
1149
    return true
14✔
1150
end
1151

1152

1153
# swap all lines intersecting the region with line above
1154
function edit_transpose_lines_up!(buf::IOBuffer, reg::Region)
6✔
1155
    b2 = beginofline(buf, first(reg))
8✔
1156
    b2 == 0 && return false
6✔
1157
    b1 = beginofline(buf, b2-1)
3✔
1158
    # we do in this order so that the buffer's position is maintained in current line
1159
    line1 = edit_splice!(buf, b1 => b2) # delete whole previous line
2✔
1160
    line1 = '\n'*line1[1:end-1] # don't include the final '\n'
4✔
1161
    pos = position(buf) # save pos in case it's at the end of line
2✔
1162
    b = endofline(buf, last(reg) - b2 + b1) # b2-b1 is the size of the removed line1
3✔
1163
    edit_splice!(buf, b => b, line1)
2✔
1164
    seek(buf, pos)
2✔
1165
    return true
2✔
1166
end
1167

1168
# swap all lines intersecting the region with line below
1169
function edit_transpose_lines_down!(buf::IOBuffer, reg::Region)
6✔
1170
    e1 = endofline(buf, last(reg))
11✔
1171
    e1 == buf.size && return false
6✔
1172
    e2 = endofline(buf, e1+1)
8✔
1173
    line2 = edit_splice!(buf, e1 => e2) # delete whole next line
5✔
1174
    line2 = line2[2:end]*'\n' # don't include leading '\n'
10✔
1175
    b = beginofline(buf, first(reg))
6✔
1176
    edit_splice!(buf, b => b, line2, rigid_mark=false)
5✔
1177
    return true
5✔
1178
end
1179

1180
# return the region if active, or the current position as a Region otherwise
1181
region_if_active(s::MIState)::Region = is_region_active(s) ? region(s) : position(s)=>position(s)
×
1182

1183
function edit_transpose_lines_up!(s::MIState)
×
1184
    set_action!(s, :edit_transpose_lines_up!)
×
1185
    if edit_transpose_lines_up!(buffer(s), region_if_active(s))
×
1186
        return refresh_line(s)
×
1187
    else
1188
        # beeping would be too noisy here
1189
        return :ignore
×
1190
    end
1191
end
1192

1193
function edit_transpose_lines_down!(s::MIState)
×
1194
    set_action!(s, :edit_transpose_lines_down!)
×
1195
    if edit_transpose_lines_down!(buffer(s), region_if_active(s))
×
1196
        return refresh_line(s)
×
1197
    else
1198
        return :ignore
×
1199
    end
1200
end
1201

1202
function edit_upper_case(s::BufferLike)
2✔
1203
    set_action!(s, :edit_upper_case)
2✔
1204
    return edit_replace_word_right(s, uppercase)
2✔
1205
end
1206
function edit_lower_case(s::BufferLike)
2✔
1207
    set_action!(s, :edit_lower_case)
2✔
1208
    return edit_replace_word_right(s, lowercase)
2✔
1209
end
1210
function edit_title_case(s::BufferLike)
2✔
1211
    set_action!(s, :edit_title_case)
2✔
1212
    return edit_replace_word_right(s, titlecase)
2✔
1213
end
1214

1215
function edit_replace_word_right(s::Union{MIState,ModeState}, replace::Function)
3✔
1216
    push_undo(s)
3✔
1217
    return edit_replace_word_right(buffer(s), replace) ? refresh_line(s) : pop_undo(s)
3✔
1218
end
1219

1220
function edit_replace_word_right(buf::IOBuffer, replace::Function)
6✔
1221
    # put the cursor at the beginning of the next word
1222
    skipchars(is_non_word_char, buf)
6✔
1223
    b = position(buf)
6✔
1224
    char_move_word_right(buf)
6✔
1225
    e = position(buf)
6✔
1226
    e == b && return false
6✔
1227
    edit_splice!(buf, b => e, replace(content(buf, b => e)))
6✔
1228
    return true
6✔
1229
end
1230

1231
edit_clear(buf::IOBuffer) = truncate(buf, 0)
3✔
1232

1233
function edit_clear(s::MIState)
12✔
1234
    set_action!(s, :edit_clear)
12✔
1235
    push_undo(s)
12✔
1236
    if push_kill!(s, edit_splice!(s, 0 => bufend(s)), false)
12✔
1237
        return refresh_line(s)
11✔
1238
    else
1239
        pop_undo(s)
1✔
1240
        return :ignore
1✔
1241
    end
1242
end
1243

1244
function replace_line(s::PromptState, l::IOBuffer)
33✔
1245
    empty_undo(s)
33✔
1246
    s.input_buffer = copy(l)
33✔
1247
    deactivate_region(s)
33✔
1248
    nothing
33✔
1249
end
1250

1251
function replace_line(s::PromptState, l::Union{String,SubString{String}}, keep_undo::Bool=false)
101✔
1252
    keep_undo || empty_undo(s)
101✔
1253
    s.input_buffer.ptr = 1
53✔
1254
    s.input_buffer.size = 0
53✔
1255
    write(s.input_buffer, l)
58✔
1256
    deactivate_region(s)
53✔
1257
    nothing
53✔
1258
end
1259

1260

1261
edit_indent_left(s::MIState, n=1) = edit_indent(s, -n)
×
1262
edit_indent_right(s::MIState, n=1) = edit_indent(s, n)
1✔
1263

1264
function edit_indent(s::MIState, num::Int)
1✔
1265
    set_action!(s, :edit_indent)
1✔
1266
    push_undo(s)
1✔
1267
    if edit_indent(buffer(s), num, is_region_active(s))
1✔
1268
        return refresh_line(s)
1✔
1269
    else
1270
        pop_undo(s)
×
1271
        return :ignore
×
1272
    end
1273
end
1274

1275
# return the indices in buffer(s) of the beginning of each lines
1276
# having a non-empty intersection with region(s)
1277
function get_lines_in_region(s::BufferLike)
4✔
1278
    buf = buffer(s)
4✔
1279
    b, e = region(buf)
8✔
1280
    bol = Int[beginofline(buf, b)] # begin of lines
5✔
1281
    while true
11✔
1282
        b = endofline(buf, b)
18✔
1283
        b >= e && break
11✔
1284
        # b < e ==> b+1 <= e <= buf.size
1285
        push!(bol, b += 1)
7✔
1286
    end
7✔
1287
    return bol
4✔
1288
end
1289

1290
# compute the number of spaces from b till the next non-space on the right
1291
# (which can also be "end of line" or "end of buffer")
1292
function leadingspaces(buf::IOBuffer, b::Int)
17✔
1293
    @views ls = something(findnext(_notspace, buf.data[1:buf.size], b+1), 0)-1
34✔
1294
    ls == -1 && (ls = buf.size)
17✔
1295
    ls -= b
17✔
1296
    return ls
17✔
1297
end
1298

1299
# indent by abs(num) characters, on the right if num >= 0, on the left otherwise
1300
# if multiline is true, indent all the lines in the region as a block.
1301
function edit_indent(buf::IOBuffer, num::Int, multiline::Bool)
19✔
1302
    bol = multiline ? get_lines_in_region(buf) : Int[beginofline(buf)]
34✔
1303
    if num < 0
19✔
1304
        # count leading spaces on the lines, which are an upper bound
1305
        # on the number of spaces characters that can be removed
1306
        ls_min = minimum(leadingspaces(buf, b) for b in bol)
12✔
1307
        ls_min == 0 && return false # can't left-indent, no space can be removed
12✔
1308
        num = -min(-num, ls_min)
8✔
1309
    end
1310
    for b in reverse!(bol) # reverse! to not mess-up the bol's offsets
15✔
1311
        _edit_indent(buf, b, num)
31✔
1312
    end
35✔
1313
    return true
15✔
1314
end
1315

1316
# indents line starting a position b by num positions
1317
# if num < 0, it is assumed that there are at least num white spaces
1318
# at the beginning of line
1319
_edit_indent(buf::IOBuffer, b::Int, num::Int) =
31✔
1320
    num >= 0 ? edit_splice!(buf, b => b, ' '^num, rigid_mark=false) :
1321
               edit_splice!(buf, b => (b - num))
1322

1323
function mode_idx(hist::HistoryProvider, mode::TextInterface)
161✔
1324
    c = :julia
×
1325
    for (k,v) in hist.mode_mapping
161✔
1326
        isequal(v, mode) && (c = k)
483✔
1327
    end
483✔
1328
    return c
161✔
1329
end
1330

1331
function guess_current_mode_name(s)
1✔
1332
    try
1✔
1333
        mode_idx(s.current_mode.hist, s.current_mode)
1✔
1334
    catch
1335
        nothing
×
1336
    end
1337
end
1338

1339
# edit current input in editor
1340
function edit_input(s, f = (filename, line, column) -> InteractiveUtils.edit(filename, line, column))
1✔
1341
    mode_name = guess_current_mode_name(s)
1✔
1342
    filename = tempname()
1✔
1343
    if mode_name === :julia
1✔
1344
        filename *= ".jl"
1✔
1345
    elseif mode_name === :shell
×
1346
        filename *= ".sh"
×
1347
    end
1348
    buf = buffer(s)
1✔
1349
    pos = position(buf)
1✔
1350
    str = String(take!(buf))
1✔
1351
    lines = readlines(IOBuffer(str); keep=true)
1✔
1352

1353
    # Compute line
1354
    line_start_offset = 0
1✔
1355
    line = 1
1✔
1356
    while line < length(lines) && line_start_offset + sizeof(lines[line]) <= pos
1✔
1357
        line_start_offset += sizeof(lines[line])
×
1358
        line += 1
×
1359
    end
×
1360

1361
    # Compute column
1362
    col = 0
1✔
1363
    off = line_start_offset
1✔
1364
    while off <= pos
8✔
1365
        off = nextind(str, off)
7✔
1366
        col += 1
7✔
1367
    end
7✔
1368

1369
    # Write current input to temp file, edit, read back
1370
    write(filename, str)
1✔
1371
    f(filename, line, col)
1✔
1372
    str_mod = readchomp(filename)
1✔
1373
    rm(filename)
1✔
1374

1375
    # Write updated content
1376
    write(buf, str_mod)
1✔
1377
    if str == str_mod
2✔
1378
        # If input was not modified: reset cursor
1379
        seek(buf, pos)
×
1380
    else
1381
        # If input was modified: move cursor to end
1382
        move_input_end(s)
1✔
1383
    end
1384
    refresh_line(s)
1✔
1385
end
1386

1387
# return the identifier under the cursor, possibly with other words concatenated
1388
# to it with dots (e.g. "A.B.C" in "X; A.B.C*3", if the cursor is between "A" and "C")
1389
function current_word_with_dots(buf::IOBuffer)
×
1390
    pos = position(buf)
×
1391
    while true
×
1392
        char_move_word_right(buf)
×
1393
        if eof(buf) || peek(buf, Char) != '.'
×
1394
            break
×
1395
        end
1396
    end
×
1397
    pend = position(buf)
×
1398
    while true
×
1399
        char_move_word_left(buf)
×
1400
        p = position(buf)
×
1401
        p == 0 && break
×
1402
        seek(buf, p-1)
×
1403
        if peek(buf, Char) != '.'
×
1404
            seek(buf, p)
×
1405
            break
×
1406
        end
1407
    end
×
1408
    pbegin = position(buf)
×
1409
    word = pend > pbegin ?
×
1410
        String(buf.data[pbegin+1:pend]) :
1411
        ""
1412
    seek(buf, pos)
×
1413
    word
×
1414
end
1415

1416
current_word_with_dots(s::MIState) = current_word_with_dots(buffer(s))
×
1417

1418
function activate_module(s::MIState)
×
1419
    word = current_word_with_dots(s);
×
1420
    isempty(word) && return beep(s)
×
1421
    try
×
1422
        mod = Base.Core.eval(Base.active_module(), Base.Meta.parse(word))
×
1423
        REPL.activate(mod)
×
1424
        edit_clear(s)
×
1425
    catch
1426
        beep(s)
×
1427
    end
1428
end
1429

1430
history_prev(::EmptyHistoryProvider) = ("", false)
×
1431
history_next(::EmptyHistoryProvider) = ("", false)
×
1432
history_first(::EmptyHistoryProvider) = ("", false)
×
1433
history_last(::EmptyHistoryProvider) = ("", false)
×
1434
history_search(::EmptyHistoryProvider, args...) = false
×
1435
add_history(::EmptyHistoryProvider, s) = nothing
×
1436
add_history(s::PromptState) = add_history(mode(s).hist, s)
115✔
1437
history_next_prefix(s, hist, prefix) = false
×
1438
history_prev_prefix(s, hist, prefix) = false
×
1439

1440
function history_prev(s::ModeState, hist)
×
1441
    l, ok = history_prev(mode(s).hist)
×
1442
    if ok
×
1443
        replace_line(s, l)
×
1444
        move_input_start(s)
×
1445
        refresh_line(s)
×
1446
    else
1447
        beep(s)
×
1448
    end
1449
    nothing
×
1450
end
1451
function history_next(s::ModeState, hist)
×
1452
    l, ok = history_next(mode(s).hist)
×
1453
    if ok
×
1454
        replace_line(s, l)
×
1455
        move_input_end(s)
×
1456
        refresh_line(s)
×
1457
    else
1458
        beep(s)
×
1459
    end
1460
    nothing
×
1461
end
1462

1463
refresh_line(s::BufferLike) = refresh_multi_line(s)
960✔
1464
refresh_line(s::BufferLike, termbuf::AbstractTerminal) = refresh_multi_line(termbuf, s)
408✔
1465

1466
default_completion_cb(::IOBuffer) = []
×
1467
default_enter_cb(_) = true
×
1468

1469
write_prompt(terminal::AbstractTerminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color)
1,253✔
1470
function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool)
1,324✔
1471
    prefix = prompt_string(p.prompt_prefix)
2,648✔
1472
    suffix = prompt_string(p.prompt_suffix)
2,648✔
1473
    write(terminal, prefix)
1,324✔
1474
    color && write(terminal, Base.text_colors[:bold])
1,324✔
1475
    width = write_prompt(terminal, p.prompt, color)
1,978✔
1476
    color && write(terminal, Base.text_colors[:normal])
1,324✔
1477
    write(terminal, suffix)
1,324✔
1478
    return width
1,324✔
1479
end
1480

1481
function write_output_prefix(io::IO, p::Prompt, color::Bool)
58✔
1482
    prefix = prompt_string(p.output_prefix_prefix)
116✔
1483
    suffix = prompt_string(p.output_prefix_suffix)
116✔
1484
    print(io, prefix)
58✔
1485
    color && write(io, Base.text_colors[:bold])
58✔
1486
    width = write_prompt(io, p.output_prefix, color)
102✔
1487
    color && write(io, Base.text_colors[:normal])
58✔
1488
    print(io, suffix)
58✔
1489
    return width
58✔
1490
end
1491

1492
# On Windows, when launching external processes, we cannot control what assumption they make on the
1493
# console mode. We thus forcibly reset the console mode at the start of the prompt to ensure they do
1494
# not leave the console mode in a corrupt state.
1495
# FIXME: remove when pseudo-tty are implemented for child processes
1496
if Sys.iswindows()
1497
function _console_mode()
×
1498
    hOutput = ccall(:GetStdHandle, stdcall, Ptr{Cvoid}, (UInt32,), -11 % UInt32) # STD_OUTPUT_HANDLE
×
1499
    dwMode = Ref{UInt32}()
×
1500
    ccall(:GetConsoleMode, stdcall, Int32, (Ref{Cvoid}, Ref{UInt32}), hOutput, dwMode)
×
1501
    return dwMode[]
×
1502
end
1503
const default_console_mode_ref = Ref{UInt32}()
1504
const default_console_mode_assigned = Ref(false)
1505
function get_default_console_mode()
×
1506
    if default_console_mode_assigned[] == false
×
1507
        default_console_mode_assigned[] = true
×
1508
        default_console_mode_ref[] = _console_mode()
×
1509
    end
1510
    return default_console_mode_ref[]
×
1511
end
1512
function _reset_console_mode()
×
1513
    mode = _console_mode()
×
1514
    if mode !== get_default_console_mode()
×
1515
        hOutput = ccall(:GetStdHandle, stdcall, Ptr{Cvoid}, (UInt32,), -11 % UInt32) # STD_OUTPUT_HANDLE
×
1516
        ccall(:SetConsoleMode, stdcall, Int32, (Ptr{Cvoid}, UInt32), hOutput, default_console_mode_ref[])
×
1517
    end
1518
    nothing
×
1519
end
1520
end
1521

1522
# returns the width of the written prompt
1523
function write_prompt(terminal::Union{IO, AbstractTerminal}, s::Union{AbstractString,Function}, color::Bool)
1,429✔
1524
    @static Sys.iswindows() && _reset_console_mode()
×
1525
    promptstr = prompt_string(s)::String
2,113✔
1526
    write(terminal, promptstr)
1,429✔
1527
    return textwidth(promptstr)
1,429✔
1528
end
1529

1530
### Keymap Support
1531

1532
const wildcard = '\U10f7ff' # "Private Use" Char
1533

1534
normalize_key(key::Char) = string(key)
2,565✔
1535
normalize_key(key::Union{Int,UInt8}) = normalize_key(Char(key))
68✔
1536
function normalize_key(key::Union{String,SubString{String}})
11,627✔
1537
    wildcard in key && error("Matching '\U10f7ff' not supported.")
23,254✔
1538
    buf = IOBuffer()
11,627✔
1539
    i = firstindex(key)
×
1540
    while i <= ncodeunits(key)
96,314✔
1541
        c, i = iterate(key, i)
73,060✔
1542
        if c == '*'
36,530✔
1543
            write(buf, wildcard)
2,547✔
1544
        elseif c == '^'
33,983✔
1545
            c, i = iterate(key, i)
3,376✔
1546
            write(buf, uppercase(c)-64)
1,688✔
1547
        elseif c == '\\'
32,295✔
1548
            c, i = iterate(key, i)
2✔
1549
            if c == 'C'
1✔
1550
                c, i = iterate(key, i)
2✔
1551
                c == '-' || error("the Control key specifier must start with \"\\\\C-\"")
1✔
1552
                c, i = iterate(key, i)
2✔
1553
                write(buf, uppercase(c)-64)
1✔
1554
            elseif c == 'M'
×
1555
                c, i = iterate(key, i)
×
1556
                c == '-' || error("the Meta key specifier must start with \"\\\\M-\"")
×
1557
                c, i = iterate(key, i)
×
1558
                write(buf, '\e')
×
1559
                write(buf, c)
1✔
1560
            end
1561
        else
1562
            write(buf, c)
32,294✔
1563
        end
1564
    end
36,530✔
1565
    return String(take!(buf))
11,627✔
1566
end
1567

1568
function normalize_keys(keymap::Union{Dict{Char,Any},AnyDict})
384✔
1569
    ret = Dict{Any,Any}()
384✔
1570
    for (k,v) in keymap
390✔
1571
        normalized = normalize_key(k)
12,428✔
1572
        if haskey(ret,normalized)
12,428✔
1573
            error("""Multiple spellings of a key in a single keymap
1✔
1574
                     (\"$k\" conflicts with existing mapping)""")
1575
        end
1576
        ret[normalized] = v
12,427✔
1577
    end
24,470✔
1578
    return ret
383✔
1579
end
1580

1581
function add_nested_key!(keymap::Dict{Char, Any}, key::Union{String, Char}, value; override::Bool = false)
37,276✔
1582
    y = iterate(key)
24,850✔
1583
    while y !== nothing
36,944✔
1584
        c, i = y
73,886✔
1585
        y = iterate(key, i)
36,944✔
1586
        if !override && c in keys(keymap) && (y === nothing || !isa(keymap[c], Dict))
36,944✔
1587
            error("Conflicting definitions for keyseq " * escape_string(key) *
×
1588
                  " within one keymap")
1589
        end
1590
        if y === nothing
36,944✔
1591
            keymap[c] = value
12,426✔
1592
            break
12,426✔
1593
        elseif !(c in keys(keymap) && isa(keymap[c], Dict))
24,518✔
1594
            keymap[c] = Dict{Char,Any}()
2,980✔
1595
        end
1596
        keymap = keymap[c]::Dict{Char, Any}
24,518✔
1597
    end
36,944✔
1598
end
1599

1600
# Redirect a key as if `seq` had been the keysequence instead in a lazy fashion.
1601
# This is different from the default eager redirect, which only looks at the current and lower
1602
# layers of the stack.
1603
struct KeyAlias
1604
    seq::String
1605
    KeyAlias(seq) = new(normalize_key(seq))
26✔
1606
end
1607

1608
function match_input(f::Function, s::Union{Nothing,MIState}, term, cs::Vector{Char}, keymap)
2,405✔
1609
    update_key_repeats(s, cs)
4,786✔
1610
    c = String(cs)
2,405✔
1611
    return function (s, p)  # s::Union{Nothing,MIState}; p can be (at least) a LineEditREPL, PrefixSearchState, Nothing
4,810✔
1612
        r = Base.invokelatest(f, s, p, c)
2,405✔
1613
        if isa(r, Symbol)
2,405✔
1614
            return r
136✔
1615
        else
1616
            return :ok
2,269✔
1617
        end
1618
    end
1619
end
1620

1621
match_input(k::Nothing, s, term, cs, keymap) = (s,p) -> return :ok
12✔
1622
match_input(k::KeyAlias, s::Union{Nothing,MIState}, term, cs, keymap::Dict{Char}) =
105✔
1623
    match_input(keymap, s, IOBuffer(k.seq), Char[], keymap)
1624

1625
function match_input(k::Dict{Char}, s::Union{Nothing,MIState}, term::Union{AbstractTerminal,IOBuffer}=terminal(s), cs::Vector{Char}=Char[], keymap::Dict{Char} = k)
7,473✔
1626
    # if we run out of characters to match before resolving an action,
1627
    # return an empty keymap function
1628
    eof(term) && return (s, p) -> :abort
14,741✔
1629
    c = read(term, Char)
2,654✔
1630
    # Ignore any `wildcard` as this is used as a
1631
    # placeholder for the wildcard (see normalize_key("*"))
1632
    c == wildcard && return (s, p) -> :ok
2,654✔
1633
    push!(cs, c)
2,654✔
1634
    key = haskey(k, c) ? c : wildcard
2,654✔
1635
    # if we don't match on the key, look for a default action then fallback on 'nothing' to ignore
1636
    return match_input(get(k, key, nothing), s, term, cs, keymap)
2,654✔
1637
end
1638

1639
update_key_repeats(s, keystroke) = nothing
×
1640
function update_key_repeats(s::MIState, keystroke::Vector{Char})
×
1641
    s.key_repeats  = s.previous_key == keystroke ? s.key_repeats + 1 : 0
2,381✔
1642
    s.previous_key = keystroke
2,381✔
1643
    return
2,381✔
1644
end
1645

1646

1647
## Conflict fixing
1648
# Consider a keymap of the form
1649
#
1650
# {
1651
#   "**" => f
1652
#   "ab" => g
1653
# }
1654
#
1655
# Naively this is transformed into a tree as
1656
#
1657
# {
1658
#   '*' => {
1659
#       '*' => f
1660
#   }
1661
#   'a' => {
1662
#       'b' => g
1663
#   }
1664
# }
1665
#
1666
# However, that's not what we want, because now "ac" is
1667
# is not defined. We need to fix this up and turn it into
1668
#
1669
# {
1670
#   '*' => {
1671
#       '*' => f
1672
#   }
1673
#   'a' => {
1674
#       '*' => f
1675
#       'b' => g
1676
#   }
1677
# }
1678
#
1679
# i.e. copy over the appropriate default subdict
1680
#
1681

1682
# deep merge where target has higher precedence
1683
function keymap_merge!(target::Dict{Char,Any}, source::Union{Dict{Char,Any},AnyDict})
2✔
1684
    for k in keys(source)
2✔
1685
        if !haskey(target, k)
2✔
1686
            target[k] = source[k]
×
1687
        elseif isa(target[k], Dict)
2✔
1688
            keymap_merge!(target[k], source[k])
×
1689
        else
1690
            # Ignore, target has higher precedence
1691
        end
×
1692
    end
2✔
1693
end
1694

1695
fixup_keymaps!(d, l, s, sk) = nothing
×
1696
function fixup_keymaps!(dict::Dict{Char,Any}, level, s, subkeymap)
1,644✔
1697
    if level > 0
1,644✔
1698
        for d in values(dict)
826✔
1699
            fixup_keymaps!(d, level-1, s, subkeymap)
2,457✔
1700
        end
4,914✔
1701
    else
1702
        if haskey(dict, s)
1,231✔
1703
            if isa(dict[s], Dict) && isa(subkeymap, Dict)
413✔
1704
                keymap_merge!(dict[s], subkeymap)
2✔
1705
            end
1706
        else
1707
            dict[s] = deepcopy(subkeymap)
818✔
1708
        end
1709
    end
1710
    nothing
1,644✔
1711
end
1712

1713
function add_specialisations(dict::Dict{Char,Any}, subdict::Dict{Char,Any}, level::Int)
2,109✔
1714
    default_branch = subdict[wildcard]
2,109✔
1715
    if isa(default_branch, Dict)
2,109✔
1716
        default_branch = default_branch::Dict{Char,Any}
411✔
1717
        # Go through all the keymaps in the default branch
1718
        # and copy them over to dict
1719
        for s in keys(default_branch)
822✔
1720
            s == wildcard && add_specialisations(dict, default_branch, level+1)
412✔
1721
            fixup_keymaps!(dict, level, s, default_branch[s])
412✔
1722
        end
2,110✔
1723
    end
1724
end
1725

1726
postprocess!(others) = nothing
×
1727
function postprocess!(dict::Dict{Char,Any})
2,666✔
1728
    # needs to be done first for every branch
1729
    if haskey(dict, wildcard)
2,666✔
1730
        add_specialisations(dict, dict, 1)
1,698✔
1731
    end
1732
    for (k,v) in dict
5,332✔
1733
        k == wildcard && continue
13,596✔
1734
        postprocess!(v)
11,898✔
1735
    end
13,596✔
1736
end
1737

1738
function getEntry(keymap::Dict{Char,Any},key::Union{String,Char})
2,606✔
1739
    v = keymap
×
1740
    for c in key
2,726✔
1741
        if !(haskey(v,c)::Bool)
2,606✔
1742
            return nothing
×
1743
        end
1744
        v = v[c]
2,606✔
1745
    end
2,606✔
1746
    return v
2,606✔
1747
end
1748

1749
# `target` is the total keymap being built up, already being a nested tree of Dicts.
1750
# source is the keymap specified by the user (with normalized keys)
1751
function keymap_merge(target::Dict{Char,Any}, source::Union{Dict{Char,Any},AnyDict})
385✔
1752
    ret = copy(target)
385✔
1753
    direct_keys = filter(p -> isa(p.second, Union{Function, KeyAlias, Nothing}), source)
12,813✔
1754
    # first direct entries
1755
    for key in keys(direct_keys)
770✔
1756
        add_nested_key!(ret, key, source[key]; override = true)
21,380✔
1757
    end
21,382✔
1758
    # then redirected entries
1759
    for key in setdiff(keys(source), keys(direct_keys))
770✔
1760
        key::Union{String, Char}
1,736✔
1761
        # We first resolve redirects in the source
1762
        value = source[key]
1,736✔
1763
        visited = Vector{Any}()
1,736✔
1764
        while isa(value, Union{Char,String})
3,470✔
1765
            value = normalize_key(value)
3,453✔
1766
            if value in visited
1,738✔
1767
                throw_eager_redirection_cycle(key)
1✔
1768
            end
1769
            push!(visited,value)
1,737✔
1770
            if !haskey(source,value)
1,737✔
1771
                break
3✔
1772
            end
1773
            value = source[value]
1,734✔
1774
        end
1,734✔
1775

1776
        if isa(value, Union{Char,String})
1,735✔
1777
            value = getEntry(ret, value)
6✔
1778
            if value === nothing
3✔
1779
                throw_could_not_find_redirected_value(key)
×
1780
            end
1781
        end
1782
        add_nested_key!(ret, key, value; override = true)
3,470✔
1783
    end
3,470✔
1784
    return ret
384✔
1785
end
1786

1787
throw_eager_redirection_cycle(key::Union{Char, String}) =
1✔
1788
    error("Eager redirection cycle detected for key ", repr(key))
1789
throw_could_not_find_redirected_value(key::Union{Char, String}) =
×
1790
    error("Could not find redirected value ", repr(key))
1791

1792
function keymap_unify(keymaps)
101✔
1793
    ret = Dict{Char,Any}()
101✔
1794
    for keymap in keymaps
101✔
1795
        ret = keymap_merge(ret, keymap)
383✔
1796
    end
382✔
1797
    postprocess!(ret)
100✔
1798
    return ret
100✔
1799
end
1800

1801
function validate_keymap(keymap)
100✔
1802
    for key in keys(keymap)
100✔
1803
        visited_keys = Any[key]
2,486✔
1804
        v = getEntry(keymap,key)
2,486✔
1805
        while isa(v,KeyAlias)
2,603✔
1806
            if v.seq in visited_keys
119✔
1807
                error("Alias cycle detected in keymap")
2✔
1808
            end
1809
            push!(visited_keys,v.seq)
117✔
1810
            v = getEntry(keymap,v.seq)
117✔
1811
        end
117✔
1812
    end
2,484✔
1813
end
1814

1815
function keymap(keymaps::Union{Vector{AnyDict},Vector{Dict{Char,Any}}})
15✔
1816
    # keymaps is a vector of prioritized keymaps, with highest priority first
1817
    ret = keymap_unify(map(normalize_keys, reverse(keymaps)))
105✔
1818
    validate_keymap(ret)
100✔
1819
    return ret
3✔
1820
end
1821

1822
const escape_defaults = merge!(
1823
    AnyDict(Char(i) => nothing for i=vcat(0:26, 28:31)), # Ignore control characters by default
1824
    AnyDict( # And ignore other escape sequences by default
1825
        "\e*" => nothing,
1826
        "\e[*" => nothing,
1827
        "\eO*" => nothing,
1828
        # Also ignore extended escape sequences
1829
        # TODO: Support ranges of characters
1830
        "\e[1**" => nothing,
1831
        "\e[2**" => nothing,
1832
        "\e[3**" => nothing,
1833
        "\e[4**" => nothing,
1834
        "\e[5**" => nothing,
1835
        "\e[6**" => nothing,
1836
        # less commonly used VT220 editing keys
1837
        "\e[2~" => nothing, # insert
1838
        "\e[3~" => nothing, # delete
1839
        "\e[5~" => nothing, # page up
1840
        "\e[6~" => nothing, # page down
1841
        # These are different spellings of arrow keys, home keys, etc.
1842
        # and should always do the same as the canonical key sequence
1843
        "\e[1~" => KeyAlias("\e[H"), # home
1844
        "\e[4~" => KeyAlias("\e[F"), # end
1845
        "\e[7~" => KeyAlias("\e[H"), # home
1846
        "\e[8~" => KeyAlias("\e[F"), # end
1847
        "\eOA"  => KeyAlias("\e[A"),
1848
        "\eOB"  => KeyAlias("\e[B"),
1849
        "\eOC"  => KeyAlias("\e[C"),
1850
        "\eOD"  => KeyAlias("\e[D"),
1851
        "\eOH"  => KeyAlias("\e[H"),
1852
        "\eOF"  => KeyAlias("\e[F"),
1853
    ),
1854
    # set mode commands
1855
    AnyDict("\e[$(c)h" => nothing for c in 1:20),
1856
    # reset mode commands
1857
    AnyDict("\e[$(c)l" => nothing for c in 1:20)
1858
    )
1859

1860
mutable struct HistoryPrompt <: TextInterface
1861
    hp::HistoryProvider
1862
    complete::CompletionProvider
1863
    keymap_dict::Dict{Char,Any}
1864
    HistoryPrompt(hp) = new(hp, EmptyCompletionProvider())
23✔
1865
end
1866

1867
mutable struct SearchState <: ModeState
1868
    terminal::AbstractTerminal
1869
    histprompt::HistoryPrompt
1870
    #rsearch (true) or ssearch (false)
1871
    backward::Bool
1872
    query_buffer::IOBuffer
1873
    response_buffer::IOBuffer
1874
    failed::Bool
1875
    ias::InputAreaState
1876
    #The prompt whose input will be replaced by the matched history
1877
    parent::Prompt
1878
    SearchState(terminal, histprompt, backward, query_buffer, response_buffer) =
23✔
1879
        new(terminal, histprompt, backward, query_buffer, response_buffer, false, InputAreaState(0,0))
1880
end
1881

1882
init_state(terminal, p::HistoryPrompt) = SearchState(terminal, p, true, IOBuffer(), IOBuffer())
23✔
1883

1884
terminal(s::SearchState) = s.terminal
74✔
1885

1886
function update_display_buffer(s::SearchState, data::ModeState)
22✔
1887
    s.failed = !history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, false)
22✔
1888
    s.failed && beep(s)
22✔
1889
    refresh_line(s)
22✔
1890
    nothing
22✔
1891
end
1892

1893
function history_next_result(s::MIState, data::ModeState)
6✔
1894
    data.failed = !history_search(data.histprompt.hp, data.query_buffer, data.response_buffer, data.backward, true)
6✔
1895
    data.failed && beep(s)
6✔
1896
    refresh_line(data)
6✔
1897
    nothing
6✔
1898
end
1899

1900
function history_set_backward(s::SearchState, backward::Bool)
×
1901
    s.backward = backward
×
1902
    nothing
×
1903
end
1904

1905
input_string(s::SearchState) = String(take!(copy(s.query_buffer)))
×
1906

1907
function reset_state(s::SearchState)
116✔
1908
    if s.query_buffer.size != 0
116✔
1909
        s.query_buffer.size = 0
×
1910
        s.query_buffer.ptr = 1
×
1911
    end
1912
    if s.response_buffer.size != 0
116✔
1913
        s.response_buffer.size = 0
×
1914
        s.response_buffer.ptr = 1
×
1915
    end
1916
    reset_state(s.histprompt.hp)
116✔
1917
    s.failed = false
116✔
1918
    nothing
116✔
1919
end
1920

1921
# a meta-prompt that presents itself as parent_prompt, but which has an independent keymap
1922
# for prefix searching
1923
mutable struct PrefixHistoryPrompt <: TextInterface
1924
    hp::HistoryProvider
1925
    parent_prompt::Prompt
1926
    complete::CompletionProvider
1927
    keymap_dict::Dict{Char,Any}
1928
    PrefixHistoryPrompt(hp, parent_prompt) =
22✔
1929
        new(hp, parent_prompt, EmptyCompletionProvider())
1930
end
1931

1932
mutable struct PrefixSearchState <: ModeState
1933
    terminal::AbstractTerminal
1934
    histprompt::PrefixHistoryPrompt
1935
    prefix::String
1936
    response_buffer::IOBuffer
1937
    ias::InputAreaState
1938
    indent::Int
1939
    # The modal interface state, if present
1940
    mi::MIState
1941
    #The prompt whose input will be replaced by the matched history
1942
    parent::Prompt
1943
    PrefixSearchState(terminal, histprompt, prefix, response_buffer) =
24✔
1944
        new(terminal, histprompt, prefix, response_buffer, InputAreaState(0,0), 0)
1945
end
1946

1947
# interface for ModeState
1948
function Base.getproperty(s::ModeState, name::Symbol)
427✔
1949
    if name === :terminal
427✔
1950
        return getfield(s, :terminal)::AbstractTerminal
6,339✔
1951
    elseif name === :prompt
427✔
1952
        return getfield(s, :prompt)::Prompt
×
1953
    elseif name === :histprompt
427✔
1954
        return getfield(s, :histprompt)::Union{HistoryPrompt,PrefixHistoryPrompt}
475✔
1955
    elseif name === :parent
420✔
1956
        return getfield(s, :parent)::Prompt
43✔
1957
    elseif name === :response_buffer
404✔
1958
        return getfield(s, :response_buffer)::IOBuffer
843✔
1959
    elseif name === :ias
370✔
1960
        return getfield(s, :ias)::InputAreaState
3,837✔
1961
    elseif name === :indent
370✔
1962
        return getfield(s, :indent)::Int
1,856✔
1963
    # # unique fields, but no harm in declaring them
1964
    # elseif name === :input_buffer
1965
    #     return getfield(s, :input_buffer)::IOBuffer
1966
    # elseif name === :region_active
1967
    #     return getfield(s, :region_active)::Symbol
1968
    # elseif name === :undo_buffers
1969
    #     return getfield(s, :undo_buffers)::Vector{IOBuffer}
1970
    # elseif name === :undo_idx
1971
    end
1972
    return getfield(s, name)
50,164✔
1973
end
1974

1975
init_state(terminal, p::PrefixHistoryPrompt) = PrefixSearchState(terminal, p, "", IOBuffer())
24✔
1976

1977
function show(io::IO, s::PrefixSearchState)
×
1978
    print(io, "PrefixSearchState ", isdefined(s,:parent) ?
×
1979
     string("(", s.parent, " active)") : "(no parent)", " for ",
1980
     isdefined(s,:mi) ? s.mi : "no MI")
1981
end
1982

1983
function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal,
1,324✔
1984
                            s::Union{PromptState,PrefixSearchState}; beeping::Bool=false)
1985
    beeping || cancel_beep(s)
1,253✔
1986
    ias = refresh_multi_line(termbuf, terminal, buffer(s), s.ias, s;
1,324✔
1987
                             indent = s.indent,
1988
                             region_active = is_region_active(s))
1989
    s.ias = ias
1,324✔
1990
    return ias
×
1991
end
1992

1993
input_string(s::PrefixSearchState) = String(take!(copy(s.response_buffer)))
41✔
1994

1995
write_prompt(terminal, s::PrefixSearchState, color::Bool) = write_prompt(terminal, s.histprompt.parent_prompt, color)
71✔
1996
prompt_string(s::PrefixSearchState) = prompt_string(s.histprompt.parent_prompt.prompt)
×
1997

1998
terminal(s::PrefixSearchState) = s.terminal
118✔
1999

2000
function reset_state(s::PrefixSearchState)
120✔
2001
    if s.response_buffer.size != 0
120✔
2002
        s.response_buffer.size = 0
8✔
2003
        s.response_buffer.ptr = 1
8✔
2004
    end
2005
    reset_state(s.histprompt.hp)
120✔
2006
    nothing
120✔
2007
end
2008

2009
function transition(f::Function, s::PrefixSearchState, mode::Prompt)
34✔
2010
    if isdefined(s, :mi)
34✔
2011
        transition(s.mi, mode)
26✔
2012
    end
2013
    s.parent = mode
34✔
2014
    s.histprompt.parent_prompt = mode
34✔
2015
    if isdefined(s, :mi)
34✔
2016
        transition(f, s.mi, s.histprompt)
26✔
2017
    else
2018
        f()
8✔
2019
    end
2020
    nothing
34✔
2021
end
2022

2023
replace_line(s::PrefixSearchState, l::IOBuffer) = (s.response_buffer = l; nothing)
4✔
2024
function replace_line(s::PrefixSearchState, l::Union{String,SubString{String}})
×
2025
    s.response_buffer.ptr = 1
30✔
2026
    s.response_buffer.size = 0
30✔
2027
    write(s.response_buffer, l)
30✔
2028
    nothing
×
2029
end
2030

2031
function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState)
46✔
2032
    buf = IOBuffer()
46✔
2033
    unsafe_write(buf, pointer(s.query_buffer.data), s.query_buffer.ptr-1)
46✔
2034
    write(buf, "': ")
46✔
2035
    offset = buf.ptr
46✔
2036
    ptr = s.response_buffer.ptr
46✔
2037
    seek(s.response_buffer, 0)
46✔
2038
    write(buf, read(s.response_buffer, String))
46✔
2039
    buf.ptr = offset + ptr - 1
46✔
2040
    s.response_buffer.ptr = ptr
46✔
2041
    failed = s.failed ? "failed " : ""
46✔
2042
    ias = refresh_multi_line(termbuf, s.terminal, buf, s.ias,
46✔
2043
                             s.backward ? "($(failed)reverse-i-search)`" : "($(failed)forward-i-search)`")
2044
    s.ias = ias
46✔
2045
    return ias
46✔
2046
end
2047

2048
state(s::MIState, p::TextInterface=mode(s)) = s.mode_state[p]
35,297✔
2049
state(s::PromptState, p::Prompt=mode(s)) = (@assert s.p == p; s)
×
2050

2051
mode(s::MIState) = s.current_mode   # ::TextInterface, and might be a Prompt
23,435✔
2052
mode(s::PromptState) = s.p          # ::Prompt
336✔
2053
mode(s::SearchState) = @assert false
×
2054
mode(s::PrefixSearchState) = s.histprompt.parent_prompt   # ::Prompt
48✔
2055

2056
setmodifiers!(s::MIState, m::Modifiers) = setmodifiers!(mode(s), m)
×
2057
setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m)
×
2058
setmodifiers!(c) = nothing
×
2059

2060
# Search Mode completions
2061
function complete_line(s::SearchState, repeats, mod::Module)
×
2062
    completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod)
×
2063
    # For now only allow exact completions in search mode
2064
    if length(completions) == 1
×
2065
        prev_pos = position(s)
×
2066
        push_undo(s)
×
2067
        edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1])
×
2068
        return true
×
2069
    end
2070
    return false
×
2071
end
2072

2073
accept_result_newmode(hp::HistoryProvider) = nothing
×
2074
function accept_result(s::MIState, p::TextInterface)
27✔
2075
    parent = something(accept_result_newmode(p.hp), state(s, p).parent)
27✔
2076
    transition(s, parent) do
27✔
2077
        replace_line(state(s, parent), state(s, p).response_buffer)
27✔
2078
        nothing
27✔
2079
    end
2080
    nothing
27✔
2081
end
2082

2083
function copybuf!(dst::IOBuffer, src::IOBuffer)
29✔
2084
    n = src.size
29✔
2085
    ensureroom(dst, n)
58✔
2086
    copyto!(dst.data, 1, src.data, 1, n)
29✔
2087
    dst.size = src.size
29✔
2088
    dst.ptr = src.ptr
29✔
2089
    nothing
29✔
2090
end
2091

2092
function enter_search(s::MIState, p::HistoryPrompt, backward::Bool)
18✔
2093
    # a bit of hack to help fix #6325
2094
    buf = copy(buffer(s))
18✔
2095
    parent = mode(s)
18✔
2096
    p.hp.last_mode = mode(s)
18✔
2097
    p.hp.last_buffer = buf
18✔
2098

2099
    transition(s, p) do
18✔
2100
        ss = state(s, p)
18✔
2101
        ss.parent = parent
18✔
2102
        ss.backward = backward
18✔
2103
        truncate(ss.query_buffer, 0)
18✔
2104
        ss.failed = false
18✔
2105
        copybuf!(ss.response_buffer, buf)
18✔
2106
    end
2107
    nothing
18✔
2108
end
2109

2110
function enter_prefix_search(s::MIState, p::PrefixHistoryPrompt, backward::Bool)
11✔
2111
    buf = copy(buffer(s))
11✔
2112
    parent = mode(s)
11✔
2113

2114
    transition(s, p) do
11✔
2115
        local pss = state(s, p)
11✔
2116
        pss.parent = parent
11✔
2117
        pss.histprompt.parent_prompt = parent
11✔
2118
        pss.prefix = String(buf.data[1:position(buf)])
11✔
2119
        copybuf!(pss.response_buffer, buf)
11✔
2120
        pss.indent = state(s, parent).indent
11✔
2121
        pss.mi = s
11✔
2122
    end
2123
    pss = state(s, p)
11✔
2124
    if backward
11✔
2125
        history_prev_prefix(pss, pss.histprompt.hp, pss.prefix)
20✔
2126
    else
2127
        history_next_prefix(pss, pss.histprompt.hp, pss.prefix)
1✔
2128
    end
2129
    nothing
11✔
2130
end
2131

2132
function setup_search_keymap(hp)
23✔
2133
    p = HistoryPrompt(hp)
23✔
2134
    pkeymap = AnyDict(
23✔
2135
        "^R"      => (s::MIState,data::ModeState,c)->(history_set_backward(data, true); history_next_result(s, data)),
2136
        "^S"      => (s::MIState,data::ModeState,c)->(history_set_backward(data, false); history_next_result(s, data)),
2137
        '\r'      => (s::MIState,o...)->accept_result(s, p),
2138
        '\n'      => '\r',
2139
        # Limited form of tab completions
2140
        '\t'      => (s::MIState,data::ModeState,c)->(complete_line(s); update_display_buffer(s, data)),
2141
        "^L"      => (s::MIState,data::ModeState,c)->(Terminals.clear(terminal(s)); update_display_buffer(s, data)),
2142

2143
        # Backspace/^H
2144
        '\b'      => (s::MIState,data::ModeState,c)->(edit_backspace(data.query_buffer) ?
2145
                        update_display_buffer(s, data) : beep(s)),
2146
        127       => KeyAlias('\b'),
2147
        # Meta Backspace
2148
        "\e\b"    => (s::MIState,data::ModeState,c)->(isempty(edit_delete_prev_word(data.query_buffer)) ?
2149
                                  beep(s) : update_display_buffer(s, data)),
2150
        "\e\x7f"  => "\e\b",
2151
        # Word erase to whitespace
2152
        "^W"      => (s::MIState,data::ModeState,c)->(isempty(edit_werase(data.query_buffer)) ?
2153
                                  beep(s) : update_display_buffer(s, data)),
2154
        # ^C and ^D
2155
        "^C"      => (s::MIState,data::ModeState,c)->(edit_clear(data.query_buffer);
2156
                       edit_clear(data.response_buffer);
2157
                       update_display_buffer(s, data);
2158
                       reset_state(data.histprompt.hp);
2159
                       transition(s, data.parent)),
2160
        "^D"      => "^C",
2161
        # Other ways to cancel search mode (it's difficult to bind \e itself)
2162
        "^G"      => "^C",
2163
        "\e\e"    => "^C",
2164
        "^K"      => (s::MIState,o...)->transition(s, state(s, p).parent),
2165
        "^Y"      => (s::MIState,data::ModeState,c)->(edit_yank(s); update_display_buffer(s, data)),
2166
        "^U"      => (s::MIState,data::ModeState,c)->(edit_clear(data.query_buffer);
2167
                     edit_clear(data.response_buffer);
2168
                     update_display_buffer(s, data)),
2169
        # Right Arrow
2170
        "\e[C"    => (s::MIState,o...)->(accept_result(s, p); edit_move_right(s)),
2171
        # Left Arrow
2172
        "\e[D"    => (s::MIState,o...)->(accept_result(s, p); edit_move_left(s)),
2173
        # Up Arrow
2174
        "\e[A"    => (s::MIState,o...)->(accept_result(s, p); edit_move_up(s)),
2175
        # Down Arrow
2176
        "\e[B"    => (s::MIState,o...)->(accept_result(s, p); edit_move_down(s)),
2177
        "^B"      => (s::MIState,o...)->(accept_result(s, p); edit_move_left(s)),
2178
        "^F"      => (s::MIState,o...)->(accept_result(s, p); edit_move_right(s)),
2179
        # Meta B
2180
        "\eb"     => (s::MIState,o...)->(accept_result(s, p); edit_move_word_left(s)),
2181
        # Meta F
2182
        "\ef"     => (s::MIState,o...)->(accept_result(s, p); edit_move_word_right(s)),
2183
        # Ctrl-Left Arrow
2184
        "\e[1;5D" => "\eb",
2185
        # Ctrl-Left Arrow on rxvt
2186
        "\eOd" => "\eb",
2187
        # Ctrl-Right Arrow
2188
        "\e[1;5C" => "\ef",
2189
        # Ctrl-Right Arrow on rxvt
2190
        "\eOc" => "\ef",
2191
        "^A"         => (s::MIState,o...)->(accept_result(s, p); move_line_start(s); refresh_line(s)),
2192
        "^E"         => (s::MIState,o...)->(accept_result(s, p); move_line_end(s); refresh_line(s)),
2193
        "^Z"      => (s::MIState,o...)->(return :suspend),
2194
        # Try to catch all Home/End keys
2195
        "\e[H"    => (s::MIState,o...)->(accept_result(s, p); move_input_start(s); refresh_line(s)),
2196
        "\e[F"    => (s::MIState,o...)->(accept_result(s, p); move_input_end(s); refresh_line(s)),
2197
        # Use ^N and ^P to change search directions and iterate through results
2198
        "^N"      => (s::MIState,data::ModeState,c)->(history_set_backward(data, false); history_next_result(s, data)),
2199
        "^P"      => (s::MIState,data::ModeState,c)->(history_set_backward(data, true); history_next_result(s, data)),
2200
        # Bracketed paste mode
2201
        "\e[200~" => (s::MIState,data::ModeState,c)-> begin
2202
            ps = state(s, mode(s))
2203
            input = readuntil(ps.terminal, "\e[201~", keep=false)
2204
            edit_insert(data.query_buffer, input); update_display_buffer(s, data)
2205
        end,
2206
        "*"       => (s::MIState,data::ModeState,c::StringLike)->(edit_insert(data.query_buffer, c); update_display_buffer(s, data))
2207
    )
2208
    p.keymap_dict = keymap([pkeymap, escape_defaults])
46✔
2209
    skeymap = AnyDict(
23✔
2210
        "^R"    => (s::MIState,o...)->(enter_search(s, p, true)),
2211
        "^S"    => (s::MIState,o...)->(enter_search(s, p, false)),
2212
    )
2213
    return (p, skeymap)
23✔
2214
end
2215

2216
keymap(state, p::Union{HistoryPrompt,PrefixHistoryPrompt}) = p.keymap_dict
10✔
2217
keymap_data(state, ::Union{HistoryPrompt, PrefixHistoryPrompt}) = state
10✔
2218

2219
Base.isempty(s::PromptState) = s.input_buffer.size == 0
61✔
2220

2221
on_enter(s::PromptState) = s.p.on_enter(s)
105✔
2222

2223
move_input_start(s::BufferLike) = (seek(buffer(s), 0); nothing)
50✔
2224
move_input_end(buf::IOBuffer) = (seekend(buf); nothing)
190✔
2225
move_input_end(s::Union{MIState,ModeState}) = (move_input_end(buffer(s)); nothing)
154✔
2226

2227
function move_line_start(s::MIState)
13✔
2228
    set_action!(s, :move_line_start)
13✔
2229
    buf = buffer(s)
13✔
2230
    curpos = position(buf)
13✔
2231
    curpos == 0 && return
13✔
2232
    if s.key_repeats > 0
12✔
2233
        move_input_start(s)
1✔
2234
    else
2235
        seek(buf, something(findprev(isequal(UInt8('\n')), buf.data, curpos), 0))
11✔
2236
    end
2237
    nothing
12✔
2238
end
2239

2240
function move_line_end(s::MIState)
31✔
2241
    set_action!(s, :move_line_end)
31✔
2242
    s.key_repeats > 0 ?
61✔
2243
        move_input_end(s) :
2244
        move_line_end(buffer(s))
2245
    nothing
31✔
2246
end
2247

2248
function move_line_end(buf::IOBuffer)
44✔
2249
    eof(buf) && return
44✔
2250
    @views pos = findnext(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)+1)
51✔
2251
    if pos === nothing
44✔
2252
        move_input_end(buf)
37✔
2253
        return
37✔
2254
    end
2255
    seek(buf, pos - 1)
7✔
2256
    nothing
7✔
2257
end
2258

2259
edit_insert_last_word(s::MIState) =
2✔
2260
    edit_insert(s, get_last_word(IOBuffer(mode(s).hist.history[end])))
2261

2262
function get_last_word(buf::IOBuffer)
14✔
2263
    move_line_end(buf)
28✔
2264
    char_move_word_left(buf)
14✔
2265
    posbeg = position(buf)
14✔
2266
    char_move_word_right(buf)
14✔
2267
    posend = position(buf)
14✔
2268
    buf = take!(buf)
14✔
2269
    word = String(buf[posbeg+1:posend])
14✔
2270
    rest = String(buf[posend+1:end])
14✔
2271
    lp, rp, lb, rb = count.(.==(('(', ')', '[', ']')), rest)
14✔
2272
    special = any(in.(('\'', '"', '`'), rest))
14✔
2273
    !special && lp == rp && lb == rb ?
14✔
2274
        word *= rest :
2275
        word
2276
end
2277

2278
function commit_line(s::MIState)
115✔
2279
    cancel_beep(s)
115✔
2280
    move_input_end(s)
115✔
2281
    refresh_line(s)
115✔
2282
    println(terminal(s))
115✔
2283
    add_history(s)
115✔
2284
    ias = InputAreaState(0, 0)
×
2285
    state(s, mode(s)).ias = ias
115✔
2286
    nothing
115✔
2287
end
2288

2289
function bracketed_paste(s::MIState; tabwidth::Int=options(s).tabwidth)
16✔
2290
    options(s).auto_indent_bracketed_paste = true
8✔
2291
    ps = state(s, mode(s))::PromptState
8✔
2292
    input = readuntil(ps.terminal, "\e[201~")
8✔
2293
    input = replace(input, '\r' => '\n')
8✔
2294
    if position(buffer(s)) == 0
8✔
2295
        indent = Base.indentation(input; tabwidth=tabwidth)[1]
7✔
2296
        input = Base.unindent(input, indent; tabwidth=tabwidth)
7✔
2297
    end
2298
    return replace(input, '\t' => " "^tabwidth)
8✔
2299
end
2300

2301
function tab_should_complete(s::MIState)
×
2302
    # Yes, we are ignoring the possibility
2303
    # the we could be in the middle of a multi-byte
2304
    # sequence, here but that's ok, since any
2305
    # whitespace we're interested in is only one byte
2306
    buf = buffer(s)
16✔
2307
    pos = position(buf)
16✔
2308
    pos == 0 && return true
16✔
2309
    c = buf.data[pos]
15✔
2310
    return c != _newline && c != UInt8('\t') &&
15✔
2311
        # hack to allow path completion in cmds
2312
        # after a space, e.g., `cd <tab>`, while still
2313
        # allowing multiple indent levels
2314
        (c != _space || pos <= 3 || buf.data[pos-1] != _space)
2315
end
2316

2317
# jump_spaces: if cursor is on a ' ', move it to the first non-' ' char on the right
2318
# if `delete_trailing`, ignore trailing ' ' by deleting them
2319
function edit_tab(s::MIState, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces)
30✔
2320
    tab_should_complete(s) && return complete_line(s)
48✔
2321
    set_action!(s, :edit_insert_tab)
12✔
2322
    push_undo(s)
12✔
2323
    edit_insert_tab(buffer(s), jump_spaces, delete_trailing) || pop_undo(s)
15✔
2324
    return refresh_line(s)
12✔
2325
end
2326

2327
function shift_tab_completion(s::MIState)
×
2328
    setmodifiers!(s, Modifiers(true))
×
2329
    return complete_line(s)
×
2330
end
2331

2332
# return true iff the content of the buffer is modified
2333
# return false when only the position changed
2334
function edit_insert_tab(buf::IOBuffer, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces)
12✔
2335
    i = position(buf)
12✔
2336
    if jump_spaces && i < buf.size && buf.data[i+1] == _space
12✔
2337
        spaces = something(findnext(_notspace, buf.data[i+1:buf.size], 1), 0)
8✔
2338
        if delete_trailing && (spaces == 0 || buf.data[i+spaces] == _newline)
7✔
2339
            edit_splice!(buf, i => (spaces == 0 ? buf.size : i+spaces-1))
3✔
2340
        else
2341
            jump = spaces == 0 ? buf.size : i+spaces-1
5✔
2342
            seek(buf, jump)
3✔
2343
            return false
3✔
2344
        end
2345
    end
2346
    # align to multiples of 4:
2347
    align = 4 - textwidth(String(buf.data[1+beginofline(buf, i):i])) % 4
9✔
2348
    edit_insert(buf, ' '^align)
12✔
2349
    return true
9✔
2350
end
2351

2352
function edit_abort(s::MIState, confirm::Bool=options(s).confirm_exit; key="^D")
51✔
2353
    set_action!(s, :edit_abort)
17✔
2354
    if !confirm || s.last_action === :edit_abort
17✔
2355
        println(terminal(s))
17✔
2356
        return :abort
17✔
2357
    else
2358
        println("Type $key again to exit.\n")
×
2359
        return refresh_line(s)
×
2360
    end
2361
end
2362

2363
const default_keymap =
2364
AnyDict(
2365
    # Tab
2366
    '\t' => (s::MIState,o...)->edit_tab(s, true),
3✔
2367
    # Shift-tab
2368
    "\e[Z" => (s::MIState,o...)->shift_tab_completion(s),
×
2369
    # Enter
2370
    '\r' => (s::MIState,o...)->begin
105✔
2371
        if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1)
105✔
2372
            commit_line(s)
105✔
2373
            return :done
105✔
2374
        else
2375
            edit_insert_newline(s)
×
2376
        end
2377
    end,
2378
    '\n' => KeyAlias('\r'),
2379
    # Backspace/^H
2380
    '\b' => (s::MIState,o...) -> is_region_active(s) ? edit_kill_region(s) : edit_backspace(s),
20✔
2381
    127 => KeyAlias('\b'),
2382
    # Meta Backspace
2383
    "\e\b" => (s::MIState,o...)->edit_delete_prev_word(s),
×
2384
    "\e\x7f" => "\e\b",
2385
    # ^D
2386
    "^D" => (s::MIState,o...)->begin
17✔
2387
        if buffer(s).size > 0
17✔
2388
            edit_delete(s)
×
2389
        else
2390
            edit_abort(s)
17✔
2391
        end
2392
    end,
2393
    # Ctrl-Space
2394
    "\0" => (s::MIState,o...)->setmark(s),
1✔
2395
    "^G" => (s::MIState,o...)->(deactivate_region(s); refresh_line(s)),
×
2396
    "^X^X" => (s::MIState,o...)->edit_exchange_point_and_mark(s),
2✔
2397
    "^B" => (s::MIState,o...)->edit_move_left(s),
×
2398
    "^F" => (s::MIState,o...)->edit_move_right(s),
×
2399
    "^P" => (s::MIState,o...)->edit_move_up(s),
×
2400
    "^N" => (s::MIState,o...)->edit_move_down(s),
×
2401
    # Meta-Up
2402
    "\e[1;3A" => (s::MIState,o...) -> edit_transpose_lines_up!(s),
×
2403
    # Meta-Down
2404
    "\e[1;3B" => (s::MIState,o...) -> edit_transpose_lines_down!(s),
×
2405
    "\e[1;2D" => (s::MIState,o...)->edit_shift_move(s, edit_move_left),
×
2406
    "\e[1;2C" => (s::MIState,o...)->edit_shift_move(s, edit_move_right),
×
2407
    "\e[1;2A" => (s::MIState,o...)->edit_shift_move(s, edit_move_up),
×
2408
    "\e[1;2B" => (s::MIState,o...)->edit_shift_move(s, edit_move_down),
×
2409
    # Meta B
2410
    "\eb" => (s::MIState,o...)->edit_move_word_left(s),
×
2411
    # Meta F
2412
    "\ef" => (s::MIState,o...)->edit_move_word_right(s),
×
2413
    # Ctrl-Left Arrow
2414
    "\e[1;5D" => "\eb",
2415
    # Ctrl-Left Arrow on rxvt
2416
    "\eOd" => "\eb",
2417
    # Ctrl-Right Arrow
2418
    "\e[1;5C" => "\ef",
2419
    # Ctrl-Right Arrow on rxvt
2420
    "\eOc" => "\ef",
2421
    # Meta Enter
2422
    "\e\r" => (s::MIState,o...)->edit_insert_newline(s),
4✔
2423
    "\e." =>  (s::MIState,o...)->edit_insert_last_word(s),
×
2424
    "\e\n" => "\e\r",
2425
    "^_" => (s::MIState,o...)->edit_undo!(s),
×
2426
    "\e_" => (s::MIState,o...)->edit_redo!(s),
×
2427
    # Simply insert it into the buffer by default
2428
    "*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c)),
2,119✔
2429
    "^U" => (s::MIState,o...)->edit_kill_line_backwards(s),
3✔
2430
    "^K" => (s::MIState,o...)->edit_kill_line_forwards(s),
×
2431
    "^Y" => (s::MIState,o...)->edit_yank(s),
2✔
2432
    "\ey" => (s::MIState,o...)->edit_yank_pop(s),
×
2433
    "\ew" => (s::MIState,o...)->edit_copy_region(s),
×
2434
    "\eW" => (s::MIState,o...)->edit_kill_region(s),
×
2435
    "^A" => (s::MIState,o...)->(move_line_start(s); refresh_line(s)),
×
2436
    "^E" => (s::MIState,o...)->(move_line_end(s); refresh_line(s)),
×
2437
    # Try to catch all Home/End keys
2438
    "\e[H"  => (s::MIState,o...)->(move_input_start(s); refresh_line(s)),
×
2439
    "\e[F"  => (s::MIState,o...)->(move_input_end(s); refresh_line(s)),
×
2440
    "^L" => (s::MIState,o...)->(Terminals.clear(terminal(s)); refresh_line(s)),
×
2441
    "^W" => (s::MIState,o...)->edit_werase(s),
×
2442
    # Meta D
2443
    "\ed" => (s::MIState,o...)->edit_delete_next_word(s),
×
2444
    "^C" => (s::MIState,o...)->begin
4✔
2445
        try # raise the debugger if present
4✔
2446
            ccall(:jl_raise_debugger, Int, ())
4✔
2447
        catch
2448
        end
2449
        cancel_beep(s)
4✔
2450
        move_input_end(s)
4✔
2451
        refresh_line(s)
4✔
2452
        print(terminal(s), "^C\n\n")
4✔
2453
        transition(s, :reset)
4✔
2454
        refresh_line(s)
4✔
2455
    end,
2456
    "^Z" => (s::MIState,o...)->(return :suspend),
×
2457
    # Right Arrow
2458
    "\e[C" => (s::MIState,o...)->edit_move_right(s),
×
2459
    # Left Arrow
2460
    "\e[D" => (s::MIState,o...)->edit_move_left(s),
×
2461
    # Up Arrow
2462
    "\e[A" => (s::MIState,o...)->edit_move_up(s),
×
2463
    # Down Arrow
2464
    "\e[B" => (s::MIState,o...)->edit_move_down(s),
×
2465
    # Meta-Right Arrow
2466
    "\e[1;3C" => (s::MIState,o...) -> edit_indent_right(s, 1),
1✔
2467
    # Meta-Left Arrow
2468
    "\e[1;3D" => (s::MIState,o...) -> edit_indent_left(s, 1),
×
2469
    # Delete
2470
    "\e[3~" => (s::MIState,o...)->edit_delete(s),
×
2471
    # Bracketed Paste Mode
2472
    "\e[200~" => (s::MIState,o...)->begin
×
2473
        input = bracketed_paste(s)
×
2474
        edit_insert(s, input)
×
2475
    end,
2476
    "^T" => (s::MIState,o...)->edit_transpose_chars(s),
×
2477
    "\et" => (s::MIState,o...)->edit_transpose_words(s),
×
2478
    "\eu" => (s::MIState,o...)->edit_upper_case(s),
×
2479
    "\el" => (s::MIState,o...)->edit_lower_case(s),
×
2480
    "\ec" => (s::MIState,o...)->edit_title_case(s),
×
2481
    "\ee" => (s::MIState,o...) -> edit_input(s),
×
2482
    "\em" => (s::MIState, o...) -> activate_module(s)
×
2483
)
2484

2485
const history_keymap = AnyDict(
2486
    "^P" => (s::MIState,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)),
×
2487
    "^N" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)),
×
2488
    "\ep" => (s::MIState,o...)->(history_prev(s, mode(s).hist)),
×
2489
    "\en" => (s::MIState,o...)->(history_next(s, mode(s).hist)),
×
2490
    # Up Arrow
2491
    "\e[A" => (s::MIState,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)),
×
2492
    # Down Arrow
2493
    "\e[B" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)),
×
2494
    # Page Up
2495
    "\e[5~" => (s::MIState,o...)->(history_prev(s, mode(s).hist)),
×
2496
    # Page Down
2497
    "\e[6~" => (s::MIState,o...)->(history_next(s, mode(s).hist)),
×
2498
    "\e<" => (s::MIState,o...)->(history_first(s, mode(s).hist)),
×
2499
    "\e>" => (s::MIState,o...)->(history_last(s, mode(s).hist)),
×
2500
)
2501

2502
const prefix_history_keymap = merge!(
2503
    AnyDict(
2504
        "^P" => (s::MIState,data::ModeState,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix),
×
2505
        "^N" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix),
×
2506
        # Up Arrow
2507
        "\e[A" => (s::MIState,data::ModeState,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix),
1✔
2508
        # Down Arrow
2509
        "\e[B" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix),
×
2510
        # by default, pass through to the parent mode
2511
        "*"    => (s::MIState,data::ModeState,c::StringLike)->begin
9✔
2512
            accept_result(s, data.histprompt);
18✔
2513
            ps = state(s, mode(s))
9✔
2514
            map = keymap(ps, mode(s))
9✔
2515
            match_input(map, s, IOBuffer(c))(s, keymap_data(ps, mode(s)))
9✔
2516
        end,
2517
        # match escape sequences for pass through
2518
        "^x*" => "*",
2519
        "\em*" => "*",
2520
        "\e*" => "*",
2521
        "\e[*" => "*",
2522
        "\eO*"  => "*",
2523
        "\e[1;5*" => "*", # Ctrl-Arrow
2524
        "\e[1;2*" => "*", # Shift-Arrow
2525
        "\e[1;3*" => "*", # Meta-Arrow
2526
        "\e[200~" => "*"
2527
    ),
2528
    # VT220 editing commands
2529
    AnyDict("\e[$(n)~" => "*" for n in 1:8),
2530
    # set mode commands
2531
    AnyDict("\e[$(c)h" => "*" for c in 1:20),
2532
    # reset mode commands
2533
    AnyDict("\e[$(c)l" => "*" for c in 1:20)
2534
)
2535

2536
function setup_prefix_keymap(hp::HistoryProvider, parent_prompt::Prompt)
22✔
2537
    p = PrefixHistoryPrompt(hp, parent_prompt)
22✔
2538
    p.keymap_dict = keymap([prefix_history_keymap])
22✔
2539
    pkeymap = AnyDict(
22✔
2540
        "^P" => (s::MIState,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)),
2541
        "^N" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)),
2542
        # Up Arrow
2543
        "\e[A" => (s::MIState,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)),
8✔
2544
        # Down Arrow
2545
        "\e[B" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)),
1✔
2546
    )
2547
    return (p, pkeymap)
22✔
2548
end
2549

2550
function deactivate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal)
284✔
2551
    clear_input_area(termbuf, s)
284✔
2552
    return s
284✔
2553
end
2554

2555
function activate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal)
408✔
2556
    s.ias = InputAreaState(0, 0)
408✔
2557
    refresh_line(s, termbuf)
408✔
2558
    nothing
408✔
2559
end
2560

2561
function activate(p::TextInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal)
124✔
2562
    @assert p == mode(s)
124✔
2563
    activate(p, state(s), termbuf, term)
124✔
2564
    nothing
124✔
2565
end
2566
activate(m::ModalInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal) =
124✔
2567
    activate(mode(s), s, termbuf, term)
2568

2569
commit_changes(t::UnixTerminal, termbuf::TerminalBuffer) = (write(t, take!(termbuf.out_stream)); nothing)
568✔
2570

2571
function transition(f::Function, s::MIState, newmode::Union{TextInterface,Symbol})
306✔
2572
    cancel_beep(s)
306✔
2573
    if newmode === :abort
306✔
2574
        s.aborted = true
18✔
2575
        return
18✔
2576
    end
2577
    if newmode === :reset
288✔
2578
        reset_state(s)
4✔
2579
        return
4✔
2580
    end
2581
    if !haskey(s.mode_state, newmode)
284✔
2582
        s.mode_state[newmode] = init_state(terminal(s), newmode)
8✔
2583
    end
2584
    termbuf = TerminalBuffer(IOBuffer())
284✔
2585
    t = terminal(s)
284✔
2586
    s.mode_state[mode(s)] = deactivate(mode(s), state(s), termbuf, t)
284✔
2587
    s.current_mode = newmode
568✔
2588
    f()
284✔
2589
    activate(newmode, state(s, newmode), termbuf, t)
284✔
2590
    commit_changes(t, termbuf)
284✔
2591
    nothing
284✔
2592
end
2593
transition(s::MIState, mode::Union{TextInterface,Symbol}) = transition((args...)->nothing, s, mode)
165✔
2594

2595
function reset_state(s::PromptState)
352✔
2596
    if s.input_buffer.size != 0
352✔
2597
        s.input_buffer.size = 0
4✔
2598
        s.input_buffer.ptr = 1
4✔
2599
    end
2600
    empty_undo(s)
352✔
2601
    deactivate_region(s)
352✔
2602
    ias = InputAreaState(0, 0)
×
2603
    s.ias = ias
352✔
2604
    return ias
352✔
2605
end
2606

2607
function reset_state(s::MIState)
120✔
2608
    for (mode, state) in s.mode_state
240✔
2609
        reset_state(state)
588✔
2610
    end
588✔
2611
end
2612

2613
const default_keymap_dict = keymap([default_keymap, escape_defaults])
2614

2615
function Prompt(prompt
154✔
2616
    ;
2617
    prompt_prefix = "",
2618
    prompt_suffix = "",
2619
    output_prefix = "",
2620
    output_prefix_prefix = "",
2621
    output_prefix_suffix = "",
2622
    keymap_dict = default_keymap_dict,
2623
    repl = nothing,
2624
    complete = EmptyCompletionProvider(),
2625
    on_enter = default_enter_cb,
2626
    on_done = ()->nothing,
2627
    hist = EmptyHistoryProvider(),
2628
    sticky = false)
2629

2630
    return Prompt(prompt, prompt_prefix, prompt_suffix, output_prefix, output_prefix_prefix, output_prefix_suffix,
77✔
2631
                   keymap_dict, repl, complete, on_enter, on_done, hist, sticky)
2632
end
2633

2634
run_interface(::Prompt) = nothing
×
2635

2636
init_state(terminal, prompt::Prompt) =
80✔
2637
    PromptState(terminal, prompt, IOBuffer(), :off, IOBuffer[], 1, InputAreaState(1, 1),
2638
                #=indent(spaces)=# -1, Threads.SpinLock(), 0.0, -Inf, nothing)
2639

2640
function init_state(terminal, m::ModalInterface)
33✔
2641
    s = MIState(m, Main, m.modes[1], false, IdDict{Any,Any}())
33✔
2642
    for mode in m.modes
33✔
2643
        s.mode_state[mode] = init_state(terminal, mode)
118✔
2644
    end
151✔
2645
    return s
33✔
2646
end
2647

2648

2649
function run_interface(terminal::TextTerminal, m::ModalInterface, s::MIState=init_state(terminal, m))
21✔
2650
    while !s.aborted
145✔
2651
        buf, ok, suspend = prompt!(terminal, m, s)
248✔
2652
        while suspend
124✔
2653
            @static if Sys.isunix(); ccall(:jl_repl_raise_sigtstp, Cint, ()); end
×
2654
            buf, ok, suspend = prompt!(terminal, m, s)
×
2655
        end
×
2656
        Base.invokelatest(mode(state(s)).on_done, s, buf, ok)
124✔
2657
    end
143✔
2658
end
2659

2660
buffer(s) = _buffer(s)::IOBuffer
2,587✔
2661
_buffer(s::PromptState) = s.input_buffer
2,407✔
2662
_buffer(s::SearchState) = s.query_buffer
×
2663
_buffer(s::PrefixSearchState) = s.response_buffer
180✔
2664
_buffer(s::IOBuffer) = s
×
2665

2666
position(s::Union{MIState,ModeState}) = position(buffer(s))
30✔
2667

2668
function empty_undo(s::PromptState)
48✔
2669
    empty!(s.undo_buffers)
433✔
2670
    s.undo_idx = 1
433✔
2671
    nothing
433✔
2672
end
2673

2674
empty_undo(s) = nothing
×
2675

2676
function push_undo(s::PromptState, advance::Bool=true)
104✔
2677
    resize!(s.undo_buffers, s.undo_idx)
4,702✔
2678
    s.undo_buffers[end] = copy(s.input_buffer)
2,348✔
2679
    advance && (s.undo_idx += 1)
2,348✔
2680
    nothing
2,348✔
2681
end
2682

2683
push_undo(s) = nothing
×
2684

2685
# must be called after a push_undo
2686
function pop_undo(s::PromptState)
×
2687
    pop!(s.undo_buffers)
5✔
2688
    s.undo_idx -= 1
5✔
2689
    nothing
5✔
2690
end
2691

2692
function edit_undo!(s::MIState)
36✔
2693
    set_action!(s, :edit_undo!)
36✔
2694
    s.last_action ∉ (:edit_redo!, :edit_undo!) && push_undo(s, false)
36✔
2695
    if !edit_undo!(state(s))
36✔
2696
        beep(s)
1✔
2697
        return :ignore
1✔
2698
    end
2699
    return nothing
35✔
2700
end
2701

2702
function edit_undo!(s::PromptState)
36✔
2703
    s.undo_idx > 1 || return false
37✔
2704
    s.input_buffer = s.undo_buffers[s.undo_idx -=1]
35✔
2705
    refresh_line(s)
35✔
2706
    return true
35✔
2707
end
2708
edit_undo!(s) = nothing
×
2709

2710
function edit_redo!(s::MIState)
6✔
2711
    set_action!(s, :edit_redo!)
6✔
2712
    if s.last_action ∉ (:edit_redo!, :edit_undo!) || !edit_redo!(state(s))
12✔
2713
        beep(s)
1✔
2714
        return :ignore
1✔
2715
    end
2716
    return nothing
5✔
2717
end
2718

2719
function edit_redo!(s::PromptState)
6✔
2720
    s.undo_idx < length(s.undo_buffers) || return false
7✔
2721
    s.input_buffer = s.undo_buffers[s.undo_idx += 1]
5✔
2722
    refresh_line(s)
5✔
2723
    return true
5✔
2724
end
2725
edit_redo!(s) = nothing
×
2726

2727
keymap(s::PromptState, prompt::Prompt) = prompt.keymap_dict
2,379✔
2728
keymap_data(s::PromptState, prompt::Prompt) = prompt.repl
2,379✔
2729
keymap(ms::MIState, m::ModalInterface) = keymap(state(ms), mode(ms))
2,380✔
2730
keymap_data(ms::MIState, m::ModalInterface) = keymap_data(state(ms), mode(ms))
2,380✔
2731

2732
function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_state(term, prompt))
124✔
2733
    Base.reseteof(term)
124✔
2734
    raw!(term, true)
124✔
2735
    enable_bracketed_paste(term)
124✔
2736
    try
124✔
2737
        activate(prompt, s, term, term)
124✔
2738
        old_state = mode(s)
124✔
2739
        while true
2,380✔
2740
            kmap = keymap(s, prompt)
2,380✔
2741
            fcn = match_input(kmap, s)
2,380✔
2742
            kdata = keymap_data(s, prompt)
2,380✔
2743
            s.current_action = :unknown # if the to-be-run action doesn't update this field,
2,380✔
2744
                                        # :unknown will be recorded in the last_action field
2745
            local status
×
2746
            # errors in keymaps shouldn't cause the REPL to fail, so wrap in a
2747
            # try/catch block
2748
            try
2,380✔
2749
                status = fcn(s, kdata)
2,380✔
2750
            catch e
2751
                @error "Error in the keymap" exception=e,catch_backtrace()
×
2752
                # try to cleanup and get `s` back to its original state before returning
2753
                transition(s, :reset)
×
2754
                transition(s, old_state)
×
2755
                status = :done
×
2756
            end
2757
            status !== :ignore && (s.last_action = s.current_action)
2,380✔
2758
            if status === :abort
2,380✔
2759
                s.aborted = true
19✔
2760
                return buffer(s), false, false
19✔
2761
            elseif status === :done
2,361✔
2762
                return buffer(s), true, false
105✔
2763
            elseif status === :suspend
2,256✔
2764
                if Sys.isunix()
×
2765
                    return buffer(s), true, true
×
2766
                end
2767
            else
2768
                @assert status ∈ (:ok, :ignore)
2,256✔
2769
            end
2770
        end
2,256✔
2771
    finally
2772
        raw!(term, false) && disable_bracketed_paste(term)
124✔
2773
    end
2774
    # unreachable
2775
end
2776

2777

2778
end # module
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc