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

JuliaLang / julia / 1459

03 Mar 2026 05:42AM UTC coverage: 76.679% (-0.1%) from 76.816%
1459

push

buildkite

web-flow
REPL: add OSC 52 clipboard fallback and graceful degradation (#61151)

When pressing Ctrl-S → Clipboard in REPL history search on a system
without clipboard tools (xsel, xclip, wl-clipboard), a
TaskFailedException stack trace was thrown. This adds:

- `OncePerId{T}`: a new callable struct that caches `initializer(key)`
per mutable key object via WeakKeyDict, for general-purpose per-object
caching
- `has_system_clipboard()` in InteractiveUtils to detect native
clipboard availability per platform
- OSC 52 terminal clipboard support as a fallback, with proper DA1
(Primary Device Attributes) query to detect terminal capability
- Graceful degradation: when neither clipboard mechanism is available,
the Clipboard option is skipped and a friendly message is shown instead
of a stack trace

Fixes #60145

Co-authored-by: Keno Fischer <Keno@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

0 of 30 new or added lines in 3 files covered. (0.0%)

950 existing lines in 17 files now uncovered.

63347 of 82613 relevant lines covered (76.68%)

23390622.35 hits per line

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

80.69
/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
using ..REPL.StylingPasses: StylingPass, SyntaxHighlightPass, RegionHighlightPass, EnclosingParenHighlightPass, StylingContext, apply_styling_passes, merge_annotations
8
using ..REPL: histsearch
9

10
using ..Terminals
11
import ..Terminals: raw!, width, height, clear_line, beep
12

13
using StyledStrings
14

15
import Base: ensureroom, show, AnyDict, position
16
using Base: something
17

18
using InteractiveUtils: InteractiveUtils
19

20
abstract type TextInterface end                # see interface immediately below
21
abstract type ModeState end                    # see interface below
22
abstract type HistoryProvider end
23
abstract type CompletionProvider end
24

25
export run_interface, Prompt, ModalInterface, transition, reset_state, edit_insert, keymap
26

27
@nospecialize # use only declared type signatures
28

29
const StringLike = Union{Char,String,SubString{String}}
30

31
mutable struct TerminalProperties
32
    da1::Union{Nothing, Vector{Int}}  # DA1 feature parameters
33
    TerminalProperties() = new(nothing)
217✔
34
end
35

36
"""
37
    receive_da1!(props::TerminalProperties, io::IO)
38

39
Read and parse a DA1 (Device Attributes) response from `io`
40
(after `\\e[?` has been consumed by the keymap).
41
Reads until `c` (DA1 terminator) or `^C` (bail-out), parses the semicolon-separated
42
parameters as integers, and stores them in `props.da1`.
43
"""
44
function receive_da1!(props::TerminalProperties, io::IO)
12✔
45
    buf = IOBuffer()
12✔
46
    while !eof(io)
60✔
47
        b = read(io, UInt8)
60✔
48
        if b == UInt8('c')  # DA1 terminator
60✔
49
            break
9✔
50
        elseif b == 0x03  # ^C bail-out
51✔
51
            break
3✔
52
        else
53
            write(buf, b)
48✔
54
        end
55
    end
48✔
56
    body = String(take!(buf))
21✔
57
    params = Int[]
12✔
58
    for part in split(body, ';')
12✔
59
        n = tryparse(Int, part)
27✔
60
        if n !== nothing
27✔
61
            push!(params, n)
24✔
62
        end
63
    end
27✔
64
    props.da1 = params
12✔
65
    return
12✔
66
end
67

68
# interface for TextInterface
69
function Base.getproperty(ti::TextInterface, name::Symbol)
743✔
70
    if name === :hp
145,158✔
71
        return getfield(ti, :hp)::HistoryProvider
458✔
72
    elseif name === :complete
144,700✔
73
        return getfield(ti, :complete)::CompletionProvider
136✔
74
    elseif name === :keymap_dict
144,564✔
75
        return getfield(ti, :keymap_dict)::Dict{Char,Any}
8,946✔
76
    end
77
    return getfield(ti, name)
135,618✔
78
end
79

80
struct ModalInterface <: TextInterface
81
    modes::Vector{TextInterface}
163✔
82
end
83

84
mutable struct Prompt <: TextInterface
85
    # A string or function to be printed as the prompt.
86
    prompt::Union{String,Function}
370✔
87
    # A string or function to be printed before the prompt. May not change the length of the prompt.
88
    # This may be used for changing the color, issuing other terminal escape codes, etc.
89
    prompt_prefix::Union{String,Function}
90
    # Same as prefix except after the prompt
91
    prompt_suffix::Union{String,Function}
92
    output_prefix::Union{String,Function}
93
    output_prefix_prefix::Union{String,Function}
94
    output_prefix_suffix::Union{String,Function}
95
    keymap_dict::Dict{Char,Any}
96
    repl::Union{AbstractREPL,Nothing}
97
    complete::CompletionProvider
98
    on_enter::Function
99
    on_done::Function
100
    hist::HistoryProvider  # TODO?: rename this `hp` (consistency with other TextInterfaces), or is the type-assert useful for mode(s)?
101
    sticky::Bool
102
    styling_passes::Vector{StylingPass}  # Styling passes to apply to input
103
end
104

UNCOV
105
show(io::IO, x::Prompt) = show(io, string("Prompt(\"", prompt_string(x.prompt), "\",...)"))
×
106

107

108
mutable struct MIState
109
    interface::ModalInterface
202✔
110
    active_module::Module
111
    previous_active_module::Module
112
    current_mode::TextInterface
113
    aborted::Bool
114
    mode_state::IdDict{TextInterface,ModeState}
115
    kill_ring::Vector{String}
116
    kill_idx::Int
117
    previous_key::Vector{Char}
118
    key_repeats::Int
119
    last_action::Symbol
120
    current_action::Symbol
121
    async_channel::Channel{Function}
122
    line_modify_lock::Base.ReentrantLock
123
    hint_generation_lock::Base.ReentrantLock
124
    n_keys_pressed::Int
125
    # Optional event that gets notified each time the prompt is ready for input
126
    prompt_ready_event::Union{Nothing, Base.Event}
127
    terminal_properties::TerminalProperties
128
end
129

130
MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}(), Base.ReentrantLock(), Base.ReentrantLock(), 0, nothing, TerminalProperties())
202✔
131

132
const BufferLike = Union{MIState,ModeState,IOBuffer}
133
const State = Union{MIState,ModeState}
134

UNCOV
135
function show(io::IO, s::MIState)
×
UNCOV
136
    print(io, "MI State (", mode(s), " active)")
×
137
end
138

139
struct InputAreaState
140
    num_rows::Int64
9,270✔
141
    curs_row::Int64
142
end
143

144
mutable struct PromptState <: ModeState
145
    terminal::AbstractTerminal
457✔
146
    p::Prompt
147
    input_buffer::IOBuffer
148
    region_active::Symbol # :shift or :mark or :off
149
    hint::Union{String,Nothing}
150
    undo_buffers::Vector{IOBuffer}
151
    undo_idx::Int
152
    ias::InputAreaState
153
    # indentation of lines which do not include the prompt
154
    # if negative, the width of the prompt is used
155
    indent::Int
156
    refresh_lock::Threads.SpinLock
157
    # this would better be Threads.Atomic{Float64}, but not supported on some platforms
158
    beeping::Float64
159
    # this option is to detect when code is pasted in non-"bracketed paste mode" :
160
    last_newline::Float64 # register when last newline was entered
161
    # this option is to speed up output
162
    refresh_wait::Union{Timer,Nothing}
163
end
164

165
struct Modifiers
166
    shift::Bool
130✔
167
end
168
Modifiers() = Modifiers(false)
130✔
169

170
options(s::PromptState) =
46,415✔
171
    if isdefined(s.p, :repl) && isdefined(s.p.repl, :options)
172
        # we can't test isa(s.p.repl, LineEditREPL) as LineEditREPL is defined
173
        # in the REPL module
174
        s.p.repl.options::Options
45,431✔
175
    else
176
        REPL.GlobalOptions::Options
852✔
177
    end
178

179
function setmark(s::MIState, guess_region_active::Bool=true)
51✔
180
    refresh = set_action!(s, :setmark)
84✔
181
    s.current_action === :setmark && s.key_repeats > 0 && activate_region(s, :mark)
42✔
182
    mark(buffer(s))
42✔
183
    refresh && refresh_line(s)
42✔
184
    nothing
42✔
185
end
186

187
# the default mark is 0
188
getmark(s::BufferLike) = max(0, buffer(s).mark)
5,375✔
189

190
const Region = Pair{Int,Int}
191

192
_region(s::BufferLike) = getmark(s) => position(s)
5,024✔
193
region(s::BufferLike) = Pair(extrema(_region(s))...)
5,024✔
194

195
bufend(s::BufferLike) = buffer(s).size
1,350✔
196

197
axes(reg::Region) = first(reg)+1:last(reg)
1,555✔
198

199
content(s::BufferLike, reg::Region = 0=>bufend(s)) = String(buffer(s).data[axes(reg)])
2,776✔
200

201
function activate_region(s::PromptState, state::Symbol)
8,422✔
202
    @assert state in (:mark, :shift, :off)
10,134✔
203
    s.region_active = state
10,134✔
204
    nothing
10,134✔
205
end
206

UNCOV
207
activate_region(s::ModeState, state::Symbol) = false
×
208
deactivate_region(s::ModeState) = activate_region(s, :off)
10,131✔
209

210
is_region_active(s::PromptState) = s.region_active in (:shift, :mark)
4,850✔
211
is_region_active(s::ModeState) = false
×
212

213
region_active(s::PromptState) = s.region_active
8,539✔
UNCOV
214
region_active(s::ModeState) = :off
×
215

216

217
input_string(s::PromptState) = takestring!(copy(s.input_buffer))::String
175✔
218

219
input_string_newlines(s::PromptState) = count(c->(c == '\n'), input_string(s))
9✔
220
function input_string_newlines_aftercursor(s::PromptState)
9✔
221
    str = input_string(s)
9✔
222
    isempty(str) && return 0
9✔
223
    rest = str[nextind(str, position(s)):end]
×
UNCOV
224
    return count(c->(c == '\n'), rest)
×
225
end
226

227
struct EmptyCompletionProvider <: CompletionProvider end
209✔
228
struct EmptyHistoryProvider <: HistoryProvider end
370✔
229

UNCOV
230
reset_state(::EmptyHistoryProvider) = nothing
×
231

232
# Before, completions were always given as strings. But at least for backslash
233
# completions, it's nice to see what glyphs are available in the completion preview.
234
# To separate between what's shown in the preview list of possible matches, and what's
235
# actually completed, we introduce this struct.
236
struct NamedCompletion
237
    completion::String # what is actually completed, for example "\trianglecdot"
337,684✔
238
    name::String # what is displayed in lists of possible completions, for example "◬ \trianglecdot"
239
end
240

UNCOV
241
NamedCompletion(completion::String) = NamedCompletion(completion, completion)
×
242

243
complete_line(c::EmptyCompletionProvider, s; hint::Bool=false) = NamedCompletion[], "", true
162✔
244

245
# complete_line can be specialized for only two arguments, when the active module
246
# doesn't matter (e.g. Pkg does this)
247
complete_line(c::CompletionProvider, s, ::Module; hint::Bool=false) = complete_line(c, s; hint)
176✔
248

UNCOV
249
terminal(s::IO) = s
×
250
terminal(s::PromptState) = s.terminal
22,291✔
251

252

253
function beep(s::PromptState, duration::Real=options(s).beep_duration,
63✔
254
              blink::Real=options(s).beep_blink,
255
              maxduration::Real=options(s).beep_maxduration;
256
              colors=options(s).beep_colors,
257
              use_current::Bool=options(s).beep_use_current)
258
    isinteractive() || return # some tests fail on some platforms
42✔
UNCOV
259
    s.beeping = min(s.beeping + duration, maxduration)
×
UNCOV
260
    let colors = Base.copymutable(colors)
×
UNCOV
261
        errormonitor(@async begin
×
262
            trylock(s.refresh_lock) || return
263
            try
264
                orig_prefix = s.p.prompt_prefix
265
                use_current && push!(colors, prompt_string(orig_prefix))
266
                i = 0
267
                while s.beeping > 0.0
268
                    prefix = colors[mod1(i+=1, end)]
269
                    s.p.prompt_prefix = prefix
270
                    refresh_multi_line(s, beeping=true)
271
                    sleep(blink)
272
                    s.beeping -= blink
273
                end
274
                s.p.prompt_prefix = orig_prefix
275
                refresh_multi_line(s, beeping=true)
276
                s.beeping = 0.0
277
            finally
278
                unlock(s.refresh_lock)
279
            end
280
        end)
281
    end
UNCOV
282
    nothing
×
283
end
284

285
function cancel_beep(s::PromptState)
5,876✔
286
    # wait till beeping finishes
287
    while !trylock(s.refresh_lock)
5,876✔
UNCOV
288
        s.beeping = 0.0
×
UNCOV
289
        sleep(.05)
×
UNCOV
290
    end
×
291
    unlock(s.refresh_lock)
5,876✔
292
    nothing
5,876✔
293
end
294

295
beep(::ModeState) = nothing
6✔
296
cancel_beep(::ModeState) = nothing
306✔
297

298
for f in Union{Symbol,Expr}[
299
          :terminal, :on_enter, :add_history, :_buffer, :(Base.isempty),
300
          :replace_line, :refresh_multi_line, :input_string, :update_display_buffer,
301
          :empty_undo, :push_undo, :pop_undo, :options, :cancel_beep, :beep,
302
          :deactivate_region, :activate_region, :is_region_active, :region_active]
303
    @eval ($f)(s::MIState, args...) = $(f)(state(s), args...)
34,435✔
304
end
305

306
for f in [:edit_insert, :edit_insert_newline, :edit_backspace, :edit_move_left,
307
          :edit_move_right, :edit_move_word_left, :edit_move_word_right]
308
    @eval function ($f)(s::MIState, args...)
168✔
309
        set_action!(s, $(Expr(:quote, f)))
8,452✔
310
        $(f)(state(s), args...)
8,470✔
311
    end
312
end
313

314
const COMMAND_GROUPS =
315
    Dict(:movement    => [:edit_move_left, :edit_move_right, :edit_move_word_left, :edit_move_word_right,
316
                          :edit_move_up, :edit_move_down, :edit_exchange_point_and_mark],
317
         :deletion    => [:edit_clear, :edit_backspace, :edit_delete, :edit_werase,
318
                          :edit_delete_prev_word,
319
                          :edit_delete_next_word,
320
                          :edit_kill_line_forwards, :edit_kill_line_backwards, :edit_kill_region],
321
         :insertion   => [:edit_insert, :edit_insert_newline, :edit_yank],
322
         :replacement => [:edit_yank_pop, :edit_transpose_chars, :edit_transpose_words,
323
                          :edit_upper_case, :edit_lower_case, :edit_title_case, :edit_indent,
324
                          :edit_transpose_lines_up!, :edit_transpose_lines_down!],
325
         :copy        => [:edit_copy_region],
326
         :misc        => [:complete_line, :setmark, :edit_undo!, :edit_redo!])
327

328
const COMMAND_GROUP = Dict{Symbol,Symbol}(command=>group for (group, commands) in COMMAND_GROUPS for command in commands)
329
command_group(command::Symbol) = get(COMMAND_GROUP, command, :nogroup)
8,431✔
330
command_group(command::Function) = command_group(nameof(command))
12✔
331

332
# return true if command should keep active a region
333
function preserve_active(command::Symbol)
8,422✔
334
    command ∈ [:edit_indent, :edit_transpose_lines_down!, :edit_transpose_lines_up!]
8,422✔
335
end
336

337
# returns whether the "active region" status changed visibly,
338
# i.e. whether there should be a visual refresh
339
function set_action!(s::MIState, command::Symbol)
9,118✔
340
    # if a command is already running, don't update the current_action field,
341
    # as the caller is used as a helper function
342
    s.current_action === :unknown || return false
9,811✔
343

344
    active = region_active(s)
8,425✔
345

346
    ## record current action
347
    s.current_action = command
8,425✔
348

349
    ## handle activeness of the region
350
    if startswith(String(command), "shift_") # shift-move command
8,425✔
351
        if active !== :shift
3✔
352
            setmark(s) # s.current_action must already have been set
3✔
353
            activate_region(s, :shift)
3✔
354
            # NOTE: if the region was already active from a non-shift
355
            # move (e.g. ^Space^Space), the region is visibly changed
356
            return active !== :off # active status is reset
3✔
357
        end
358
    elseif !(preserve_active(command) ||
16,841✔
359
             command_group(command) === :movement && region_active(s) === :mark)
360
        # if we move after a shift-move, the region is de-activated
361
        # (e.g. like emacs behavior)
362
        deactivate_region(s)
8,419✔
363
        return active !== :off
8,419✔
364
    end
365
    false
366
end
367

368
set_action!(s, command::Symbol) = nothing
9✔
369

UNCOV
370
common_prefix(completions::Vector{NamedCompletion}) = common_prefix(map(x -> x.completion, completions))
×
UNCOV
371
function common_prefix(completions::Vector{String})
×
UNCOV
372
    ret = ""
×
UNCOV
373
    c1 = completions[1]
×
UNCOV
374
    isempty(c1) && return ret
×
UNCOV
375
    i = 1
×
UNCOV
376
    cc, nexti = iterate(c1, i)
×
UNCOV
377
    while true
×
UNCOV
378
        for c in completions
×
UNCOV
379
            (i > lastindex(c) || c[i] != cc) && return ret
×
UNCOV
380
        end
×
UNCOV
381
        ret = string(ret, cc)
×
UNCOV
382
        i >= lastindex(c1) && return ret
×
UNCOV
383
        i = nexti
×
UNCOV
384
        cc, nexti = iterate(c1, i)
×
UNCOV
385
    end
×
386
end
387

388
# This is the maximum number of completions that will be displayed in a single
389
# column, anything above that and multiple columns will be used. Note that this
390
# does not restrict column length when multiple columns are used.
391
const MULTICOLUMN_THRESHOLD = 5
392

UNCOV
393
show_completions(s::PromptState, completions::Vector{NamedCompletion}) = show_completions(s, map(x -> x.name, completions))
×
394

395
# Show available completions
396
function show_completions(s::PromptState, completions::Vector{String})
9✔
397
    # skip any lines of input after the cursor
398
    cmove_down(terminal(s), input_string_newlines_aftercursor(s))
9✔
399
    println(terminal(s))
9✔
400
    if any(Base.Fix1(occursin, '\n'), completions)
39✔
401
        foreach(Base.Fix1(println, terminal(s)), completions)
3✔
402
    else
403
        n = length(completions)
6✔
404
        colmax = 2 + maximum(length, completions; init=1) # n.b. length >= textwidth
12✔
405

406
        num_cols = min(cld(n, MULTICOLUMN_THRESHOLD),
6✔
407
                       max(div(width(terminal(s)), colmax), 1))
408

409
        entries_per_col = cld(n, num_cols)
6✔
410
        idx = 0
6✔
411
        for _ in 1:entries_per_col
6✔
412
            for col = 0:(num_cols-1)
18✔
413
                idx += 1
27✔
414
                idx > n && break
27✔
415
                cmove_col(terminal(s), colmax*col+1)
27✔
416
                print(terminal(s), completions[idx])
27✔
417
            end
27✔
418
            println(terminal(s))
18✔
419
        end
18✔
420
    end
421

422
    # make space for the prompt
423
    for i = 1:input_string_newlines(s)
9✔
UNCOV
424
        println(terminal(s))
×
UNCOV
425
    end
×
426
end
427

428
# Prompt Completions & Hints
429
function complete_line(s::MIState)
18✔
430
    set_action!(s, :complete_line)
18✔
431
    if complete_line(state(s), s.key_repeats, s.active_module)
18✔
432
        return refresh_line(s)
15✔
433
    else
434
        beep(s)
3✔
435
        return :ignore
3✔
436
    end
437
end
438

439
# Old complete_line return type: Vector{String},          String, Bool
440
# New complete_line return type: NamedCompletion{String}, String, Bool
441
#                            OR  NamedCompletion{String}, Region, Bool
442
#
443
# due to close coupling of the Pkg ReplExt `complete_line` can still return a vector of strings,
444
# so we convert those in this helper
445
function complete_line_named(c, s, args...; kwargs...)::Tuple{Vector{NamedCompletion},Region,Bool}
272✔
446
    r1, r2, should_complete = complete_line(c, s, args...; kwargs...)::Union{
272✔
447
        Tuple{Vector{String}, String, Bool},
448
        Tuple{Vector{NamedCompletion}, String, Bool},
449
        Tuple{Vector{NamedCompletion}, Region, Bool},
450
    }
451
    completions = (r1 isa Vector{String} ? map(NamedCompletion, r1) : r1)
136✔
452
    r = (r2 isa String ? (position(s)-sizeof(r2) => position(s)) : r2)
191✔
453
    completions, r, should_complete
136✔
454
end
455

456
# checks for a hint and shows it if appropriate.
457
# to allow the user to type even if hint generation is slow, the
458
# hint is generated on a worker thread, and only shown if the user hasn't
459
# pressed a key since the hint generation was requested
460
function check_show_hint(s::MIState)
7,990✔
461
    st = state(s)
7,990✔
462

463
    this_key_i = s.n_keys_pressed
7,990✔
464
    next_key_pressed() = @lock s.line_modify_lock s.n_keys_pressed > this_key_i
16,334✔
465
    function lock_clear_hint()
8,167✔
466
        @lock s.line_modify_lock begin
177✔
467
            next_key_pressed() || s.aborted || clear_hint(st) && refresh_line(s)
354✔
468
        end
469
    end
470

471
    if !options(st).hint_tab_completes || !eof(buffer(st))
16,055✔
472
        # only generate hints if enabled and at the end of the line
473
        # TODO: maybe show hints for insertions at other positions
474
        # Requires making space for them earlier in refresh_multi_line
475
        lock_clear_hint()
69✔
476
        return
69✔
477
    end
478
    t_completion = Threads.@spawn :default begin
15,842✔
479
        named_completions, reg, should_complete = nothing, nothing, nothing
7,921✔
480

481
        # only allow one task to generate hints at a time and check around lock
482
        # if the user has pressed a key since the hint was requested, to skip old completions
483
        next_key_pressed() && return
7,921✔
484
        @lock s.hint_generation_lock begin
118✔
485
            next_key_pressed() && return
118✔
486
            named_completions, reg, should_complete = try
118✔
487
                complete_line_named(st.p.complete, st, s.active_module; hint = true)
118✔
488
            catch
UNCOV
489
                lock_clear_hint()
×
490
                return
118✔
491
            end
492
        end
493
        next_key_pressed() && return
118✔
494

495
        completions = map(x -> x.completion, named_completions)
1,029✔
496
        if isempty(completions)
118✔
497
            lock_clear_hint()
97✔
498
            return
97✔
499
        end
500
        # Don't complete for single chars, given e.g. `x` completes to `xor`
501
        if reg.second - reg.first > 1 && should_complete
21✔
502
            singlecompletion = length(completions) == 1
10✔
503
            p = singlecompletion ? completions[1] : common_prefix(completions)
10✔
504
            if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
10✔
505
                # The completion `p` and the region `reg` may not share the same initial
506
                # characters, for instance when completing to subscripts or superscripts.
507
                # So, in general, make sure that the hint starts at the correct position by
508
                # incrementing its starting position by as many characters as the input.
509
                maxind = lastindex(p)
20✔
510
                startind = sizeof(content(s, reg))
10✔
511
                if startind ≤ maxind # completion on a complete name returns itself so check that there's something to hint
10✔
512
                    # index of p from which to start providing the hint
513
                    startind = nextind(p, startind)
20✔
514
                    hint = p[startind:end]
19✔
515
                    next_key_pressed() && return
10✔
516
                    @lock s.line_modify_lock begin
10✔
517
                        if !s.aborted
10✔
518
                            state(s).hint = hint
10✔
519
                            refresh_line(s)
10✔
520
                        end
521
                    end
522
                    return
10✔
523
                end
524
            end
525
        end
526
        lock_clear_hint()
11✔
527
    end
528
    Base.errormonitor(t_completion)
7,921✔
529
    return
7,921✔
530
end
531

532
function clear_hint(s::ModeState)
533
    if !isnothing(s.hint)
180✔
534
        s.hint = "" # don't set to nothing here. That will be done in `maybe_show_hint`
3✔
535
        return true # indicate maybe_show_hint has work to do
3✔
536
    else
537
        return false
174✔
538
    end
539
end
540

541
function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false)
36✔
542
    completions, reg, should_complete = complete_line_named(s.p.complete, s, mod; hint)
18✔
543
    isempty(completions) && return false
18✔
544
    if !should_complete
15✔
545
        # should_complete is false for cases where we only want to show
546
        # a list of possible completions but not complete, e.g. foo(\t
UNCOV
547
        show_completions(s, completions)
×
548
    elseif length(completions) == 1
15✔
549
        # Replace word by completion
550
        push_undo(s)
15✔
551
        edit_splice!(s, reg, completions[1].completion)
15✔
552
    else
UNCOV
553
        p = common_prefix(completions)
×
UNCOV
554
        partial = content(s, reg.first => min(bufend(s), reg.first + sizeof(p)))
×
UNCOV
555
        if !isempty(p) && p != partial
×
556
            # All possible completions share the same prefix, so we might as
557
            # well complete that.
558
            push_undo(s)
×
UNCOV
559
            edit_splice!(s, reg, p)
×
UNCOV
560
        elseif repeats > 0
×
UNCOV
561
            show_completions(s, completions)
×
562
        end
563
    end
564
    return true
15✔
565
end
566

567
function clear_input_area(terminal::AbstractTerminal, s::PromptState)
627✔
568
    if s.refresh_wait !== nothing
627✔
UNCOV
569
        close(s.refresh_wait)
×
UNCOV
570
        s.refresh_wait = nothing
×
571
    end
572
    _clear_input_area(terminal, s.ias)
627✔
573
    s.ias = InputAreaState(0, 0)
627✔
574
end
575
clear_input_area(terminal::AbstractTerminal, s::ModeState) = (_clear_input_area(terminal, s.ias); s.ias = InputAreaState(0, 0))
105✔
UNCOV
576
clear_input_area(s::ModeState) = clear_input_area(s.terminal, s)
×
577

578
function _clear_input_area(terminal::AbstractTerminal, state::InputAreaState)
5,699✔
579
    # Go to the last line
580
    if state.curs_row < state.num_rows
5,699✔
581
        cmove_down(terminal, state.num_rows - state.curs_row)
114✔
582
    end
583

584
    # Clear lines one by one going up
585
    for j = 2:state.num_rows
5,699✔
586
        clear_line(terminal)
5,563✔
587
        cmove_up(terminal)
5,563✔
588
    end
8,893✔
589

590
    # Clear top line
591
    clear_line(terminal)
5,699✔
592
    nothing
5,699✔
593
end
594

UNCOV
595
prompt_string(s::PromptState) = prompt_string(s.p)
×
UNCOV
596
prompt_string(p::Prompt) = prompt_string(p.prompt)
×
597
prompt_string(s::AbstractString) = s
14,703✔
598
prompt_string(f::Function) = Base.invokelatest(f)
8,797✔
599

600
function maybe_show_hint(s::PromptState)
3,725✔
601
    isa(s.hint, String) || return nothing
7,431✔
602
    # The hint being "" then nothing is used to first clear a previous hint, then skip printing the hint
603
    if isempty(s.hint)
38✔
604
        s.hint = nothing
10✔
605
    else
606
        Base.printstyled(terminal(s), s.hint, color=:light_black)
9✔
607
        cmove_left(terminal(s), textwidth(s.hint))
9✔
608
        s.hint = "" # being "" signals to do one clear line remainder to clear the hint next time the screen is refreshed
9✔
609
    end
610
    return nothing
19✔
611
end
612

613
max_highlight_size::Int = 10000 # bytes
614

615
function refresh_multi_line(s::PromptState; kw...)
7,450✔
616
    if s.refresh_wait !== nothing
3,725✔
UNCOV
617
        close(s.refresh_wait)
×
UNCOV
618
        s.refresh_wait = nothing
×
619
    end
620
    if s.hint isa String
3,725✔
621
        # clear remainder of line which is unknown here if it had a hint before unbeknownst to refresh_multi_line
622
        # the clear line cannot be printed each time because it would break column movement
623
        print(terminal(s), "\e[0K")
19✔
624
    end
625
    r = refresh_multi_line(terminal(s), s; kw...)
3,725✔
626
    maybe_show_hint(s) # now maybe write the hint back to the screen
3,725✔
627
    return r
3,725✔
628
end
629
refresh_multi_line(s::ModeState; kw...) = refresh_multi_line(terminal(s), s; kw...)
192✔
630
refresh_multi_line(termbuf::TerminalBuffer, s::ModeState; kw...) = refresh_multi_line(termbuf, terminal(s), s; kw...)
1,464✔
UNCOV
631
refresh_multi_line(termbuf::TerminalBuffer, term, s::ModeState; kw...) = (@assert term === terminal(s); refresh_multi_line(termbuf,s; kw...))
×
632

633
function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf::IOBuffer,
9,934✔
634
                            state::InputAreaState, prompt = "";
635
                            indent::Int = 0, region_active::Bool = false)
636
    _clear_input_area(termbuf, state)
4,967✔
637

638
    cols = width(terminal)
4,967✔
639
    rows = height(terminal)
4,967✔
640
    curs_row = -1 # relative to prompt (1-based)
4,967✔
641
    curs_pos = -1 # 1-based column position of the cursor
4,967✔
642
    cur_row = 0   # count of the number of rows
4,967✔
643
    buf_pos = position(buf)
4,967✔
644
    line_pos = buf_pos
4,967✔
645
    regstart, regstop = region(buf)
4,967✔
646
    written = 0
4,967✔
647
    @static if Sys.iswindows()
648
        writer = Terminals.pipe_writer(terminal)
1,438✔
649
        if writer isa Base.TTY && !Base.ispty(writer)::Bool
1,438✔
650
            _reset_console_mode(writer.handle)
651
        end
652
    end
653
    # Write out the prompt string
654
    lindent = write_prompt(termbuf, prompt, hascolor(terminal))::Int
4,967✔
655
    # Count the '\n' at the end of the line if the terminal emulator does (specific to DOS cmd prompt)
656
    miscountnl = @static if Sys.iswindows()
657
        reader = Terminals.pipe_reader(terminal)
1,438✔
658
        reader isa Base.TTY && !Base.ispty(reader)::Bool
1,438✔
659
    else false end
3,529✔
660

661
    # Get the styling passes from the prompt
662
    prompt_obj = nothing
4,967✔
663
    if prompt isa PromptState
4,967✔
664
        prompt_obj = prompt.p
4,763✔
665
    elseif prompt isa PrefixSearchState
204✔
666
        if isdefined(prompt, :parent) && prompt.parent isa Prompt
201✔
667
            prompt_obj = prompt.parent
201✔
668
        end
669
    end
670

671
    styled_buffer = AnnotatedString("")
4,967✔
672
    if buf.size > 0 && buf.size <= max_highlight_size
4,967✔
673
        full_input = String(buf.data[1:buf.size])
7,908✔
674
        if !isempty(full_input)
3,954✔
675
            passes = StylingPass[]
3,954✔
676
            context = StylingContext(buf_pos, regstart, regstop)
3,954✔
677

678
            # Add prompt-specific styling passes if the prompt has them and styling is enabled
679
            enable_style_input = prompt_obj === nothing ? false :
7,905✔
680
                (isdefined(prompt_obj, :repl) && prompt_obj.repl !== nothing ?
681
                    prompt_obj.repl.options.style_input : false)
682

683
            if enable_style_input && prompt_obj !== nothing
3,954✔
684
                append!(passes, prompt_obj.styling_passes)
936✔
685
            end
686

687
            if region_active
3,954✔
688
                push!(passes, RegionHighlightPass())
12✔
689
            end
690

691
            if !isempty(passes)
3,954✔
692
                styled_buffer = apply_styling_passes(full_input, passes, context)
960✔
693
            end
694
        end
695
    end
696

697
    # Now go through the buffer line by line
698
    seek(buf, 0)
9,934✔
699
    moreinput = true # add a blank line if there is a trailing newline on the last line
4,967✔
700
    lastline = false # indicates when to stop printing lines, even when there are potentially
4,967✔
701
                     # more (for the case where rows is too small to print everything)
702
                     # Note: when there are too many lines for rows, we still print the first lines
703
                     # even if they are going to not be visible in the end: for simplicity, but
704
                     # also because it does the 'right thing' when the window is resized
705
    while moreinput
10,891✔
706
        line = readline(buf, keep=true)
5,924✔
707
        moreinput = endswith(line, "\n")
5,924✔
708
        if rows == 1 && line_pos <= sizeof(line) - moreinput
5,924✔
709
            # we special case rows == 1, as otherwise by the time the cursor is seen to
710
            # be in the current line, it's too late to chop the '\n' away
UNCOV
711
            lastline = true
×
UNCOV
712
            curs_row = 1
×
UNCOV
713
            curs_pos = lindent + line_pos
×
714
        end
715
        if moreinput && lastline # we want to print only one "visual" line, so
5,924✔
UNCOV
716
            line = chomp(line)   # don't include the trailing "\n"
×
717
        end
718
        # We need to deal with on-screen characters, so use textwidth to compute occupied columns
719
        llength = textwidth(line)
11,848✔
720
        slength = sizeof(line)
11,848✔
721
        cur_row += 1
5,924✔
722

723
        # Extract the portion of styled_buffer corresponding to this line.
724
        if !isempty(styled_buffer)
5,924✔
725
            # Calculate byte positions for this line in the buffer
726
            line_start_byte = written + 1
651✔
727
            line_end_byte = written + slength
651✔
728

729
            # Convert to valid character indices (handles UTF-8 boundaries)
730
            start_idx = thisind(styled_buffer, line_start_byte)
651✔
731
            end_idx = thisind(styled_buffer, line_end_byte)
651✔
732

733
            lwrite = @view styled_buffer[start_idx:end_idx]
660✔
734
        else
735
            lwrite = line
5,273✔
736
        end
737

738
        written += slength
5,924✔
739
        cmove_col(termbuf, lindent + 1)
5,924✔
740

741
        write(IOContext(termbuf, :color => hascolor(terminal)), lwrite)
11,197✔
742
        # We expect to be line after the last valid output line (due to
743
        # the '\n' at the end of the previous line)
744
        if curs_row == -1
5,924✔
745
            line_pos -= slength # '\n' gets an extra pos
5,798✔
746
            # in this case, we haven't yet written the cursor position
747
            if line_pos < 0 || !moreinput
10,930✔
748
                num_chars = line_pos >= 0 ?
5,633✔
749
                                llength :
750
                                textwidth(line[1:prevind(line, line_pos + slength + 1)])
751
                curs_row, curs_pos = divrem(lindent + num_chars - 1, cols)
4,967✔
752
                curs_row += cur_row
4,967✔
753
                curs_pos += 1
4,967✔
754
                # There's an issue if the cursor is after the very right end of the screen. In that case we need to
755
                # move the cursor to the next line, and emit a newline if needed
756
                if curs_pos == cols
4,967✔
757
                    # only emit the newline if the cursor is at the end of the line we're writing
758
                    if line_pos == 0
27✔
759
                        write(termbuf, "\n")
27✔
760
                        cur_row += 1
27✔
761
                    end
762
                    curs_row += 1
27✔
763
                    curs_pos = 0
27✔
764
                    cmove_col(termbuf, 1)
27✔
765
                end
766
            end
767
        end
768
        cur_row += div(max(lindent + llength + miscountnl - 1, 0), cols)
5,924✔
769
        lindent = indent < 0 ? lindent : indent
5,924✔
770

771
        lastline && break
5,924✔
772
        if curs_row >= 0 && cur_row + 1 >= rows &&             # when too many lines,
5,924✔
773
                            cur_row - curs_row + 1 >= rows ÷ 2 # center the cursor
UNCOV
774
            lastline = true
×
775
        end
776
    end
5,924✔
777
    seek(buf, buf_pos)
9,934✔
778

779
    # Let's move the cursor to the right position
780
    # The line first
781
    n = cur_row - curs_row
4,967✔
782
    if n > 0
4,967✔
783
        cmove_up(termbuf, n)
120✔
784
    end
785

786
    #columns are 1 based
787
    cmove_col(termbuf, curs_pos + 1)
4,967✔
788
    # Updated cur_row,curs_row
789
    return InputAreaState(cur_row, curs_row)
4,967✔
790
end
791

792
function refresh_multi_line(terminal::UnixTerminal, args...; kwargs...)
8,452✔
793
    outbuf = IOBuffer()
4,226✔
794
    termbuf = TerminalBuffer(outbuf)
4,226✔
795
    ret = refresh_multi_line(termbuf, terminal, args...;kwargs...)
4,226✔
796
    # Output the entire refresh at once
797
    write(terminal, take!(outbuf))
4,226✔
798
    flush(terminal)
4,226✔
799
    return ret
4,226✔
800
end
801

802

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

806
function reset_key_repeats(f::Function, s::MIState)
60✔
807
    key_repeats_sav = s.key_repeats
60✔
808
    try
60✔
809
        s.key_repeats = 0
60✔
810
        return f()
60✔
811
    finally
812
        s.key_repeats = key_repeats_sav
60✔
813
    end
814
end
815

816
function edit_exchange_point_and_mark(s::MIState)
15✔
817
    set_action!(s, :edit_exchange_point_and_mark)
15✔
818
    return edit_exchange_point_and_mark(buffer(s)) ? refresh_line(s) : false
15✔
819
end
820

821
function edit_exchange_point_and_mark(buf::IOBuffer)
6✔
822
    m = getmark(buf)
21✔
823
    m == position(buf) && return false
21✔
824
    mark(buf)
21✔
825
    seek(buf, m)
42✔
826
    return true
21✔
827
end
828

UNCOV
829
char_move_left(s::PromptState) = char_move_left(s.input_buffer)
×
830
function char_move_left(buf::IOBuffer)
1,344✔
831
    while position(buf) > 0
1,344✔
832
        seek(buf, position(buf)-1)
2,892✔
833
        c = peek(buf)
1,446✔
834
        (((c & 0x80) == 0) || ((c & 0xc0) == 0xc0)) && break
1,641✔
835
    end
102✔
836
    pos = position(buf)
1,344✔
837
    c = read(buf, Char)
1,344✔
838
    seek(buf, pos)
2,688✔
839
    return c
1,344✔
840
end
841

842
function edit_move_left(buf::IOBuffer)
291✔
843
    if position(buf) > 0
291✔
844
        #move to the next base UTF8 character to the left
845
        while true
297✔
846
            c = char_move_left(buf)
297✔
847
            if textwidth(c) != 0 || c == '\n' || position(buf) == 0
330✔
848
                break
291✔
849
            end
850
        end
6✔
851
        return true
291✔
852
    end
UNCOV
853
    return false
×
854
end
855

856
edit_move_left(s::PromptState) = edit_move_left(s.input_buffer) ? refresh_line(s) : false
195✔
857

858
function edit_move_word_left(s::PromptState)
859
    if position(s) > 0
12✔
860
        char_move_word_left(s.input_buffer)
12✔
861
        return refresh_line(s)
12✔
862
    end
UNCOV
863
    return nothing
×
864
end
865

UNCOV
866
char_move_right(s::MIState) = char_move_right(buffer(s))
×
867
function char_move_right(buf::IOBuffer)
6✔
868
    return !eof(buf) && read(buf, Char)
666✔
869
end
870

871
function char_move_word_right(buf::IOBuffer, is_delimiter::Function=is_non_word_char)
192✔
872
    while !eof(buf) && is_delimiter(char_move_right(buf))
420✔
873
    end
36✔
874
    while !eof(buf)
414✔
875
        pos = position(buf)
372✔
876
        if is_delimiter(char_move_right(buf))
372✔
877
            seek(buf, pos)
300✔
878
            break
150✔
879
        end
880
    end
414✔
881
end
882

883
function char_move_word_left(buf::IOBuffer, is_delimiter::Function=is_non_word_char)
213✔
884
    while position(buf) > 0 && is_delimiter(char_move_left(buf))
606✔
885
    end
195✔
886
    while position(buf) > 0
471✔
887
        pos = position(buf)
423✔
888
        if is_delimiter(char_move_left(buf))
423✔
889
            seek(buf, pos)
324✔
890
            break
162✔
891
        end
892
    end
471✔
893
end
894

895
char_move_word_right(s::Union{MIState,ModeState}) = char_move_word_right(buffer(s))
3✔
UNCOV
896
char_move_word_left(s::Union{MIState,ModeState}) = char_move_word_left(buffer(s))
×
897

898
function edit_move_right(buf::IOBuffer)
66✔
899
    if !eof(buf)
66✔
900
        # move to the next base UTF8 character to the right
901
        while true
66✔
902
            c = char_move_right(buf)
66✔
903
            eof(buf) && break
66✔
904
            pos = position(buf)
33✔
905
            nextc = read(buf,Char)
33✔
906
            seek(buf,pos)
66✔
907
            (textwidth(nextc) != 0 || nextc == '\n') && break
36✔
908
        end
3✔
909
        return true
63✔
910
    end
911
    return false
3✔
912
end
913
function edit_move_right(m::MIState)
6✔
914
    s = state(m)
6✔
915
    buf = s.input_buffer
6✔
916
    if edit_move_right(s.input_buffer)
6✔
917
        refresh_line(s)
6✔
918
        return true
6✔
919
    else
UNCOV
920
        completions, reg, should_complete = complete_line(s.p.complete, s, m.active_module)
×
UNCOV
921
        if should_complete && eof(buf) && length(completions) == 1 && reg.second - reg.first > 1
×
922
            # Replace word by completion
UNCOV
923
            prev_pos = position(s)
×
UNCOV
924
            push_undo(s)
×
UNCOV
925
            edit_splice!(s, (prev_pos - reg.second + reg.first) => prev_pos, completions[1].completion)
×
UNCOV
926
            refresh_line(state(s))
×
UNCOV
927
            return true
×
928
        else
UNCOV
929
            return false
×
930
        end
931
    end
932
end
933

934
function edit_move_word_right(s::PromptState)
935
    if !eof(s.input_buffer)
3✔
936
        char_move_word_right(s)
3✔
937
        return refresh_line(s)
3✔
938
    end
UNCOV
939
    return nothing
×
940
end
941

942
## Move line up/down
943
# Querying the terminal is expensive, memory access is cheap
944
# so to find the current column, we find the offset for the start
945
# of the line.
946

947
function edit_move_up(buf::IOBuffer)
111✔
948
    npos = findprev(isequal(UInt8('\n')), buf.data, position(buf))
192✔
949
    npos === nothing && return false # we're in the first line
111✔
950
    # We're interested in character count, not byte count
951
    offset = length(content(buf, npos => position(buf)))
51✔
952
    npos2 = something(findprev(isequal(UInt8('\n')), buf.data, npos-1), 0)
66✔
953
    seek(buf, npos2)
102✔
954
    for _ = 1:offset
51✔
955
        pos = position(buf)
165✔
956
        if read(buf, Char) == '\n'
165✔
957
            seek(buf, pos)
24✔
958
            break
12✔
959
        end
960
    end
153✔
961
    return true
51✔
962
end
963
function edit_move_up(s::MIState)
964
    set_action!(s, :edit_move_up)
36✔
965
    changed = edit_move_up(buffer(s))
36✔
966
    changed && refresh_line(s)
36✔
967
    return changed
36✔
968
end
969

970
function edit_move_down(buf::IOBuffer)
90✔
971
    npos = something(findprev(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)), 0)
141✔
972
    # We're interested in character count, not byte count
973
    offset = length(String(buf.data[(npos+1):(position(buf))]))
159✔
974
    npos2 = findnext(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)+1)
171✔
975
    if npos2 === nothing #we're in the last line
90✔
976
        return false
18✔
977
    end
978
    seek(buf, npos2)
144✔
979
    for _ = 1:offset
72✔
980
        pos = position(buf)
261✔
981
        if eof(buf) || read(buf, Char) == '\n'
489✔
982
            seek(buf, pos)
66✔
983
            break
33✔
984
        end
985
    end
228✔
986
    return true
72✔
987
end
988
function edit_move_down(s::MIState)
989
    set_action!(s, :edit_move_down)
15✔
990
    changed = edit_move_down(buffer(s))
15✔
991
    changed && refresh_line(s)
15✔
992
    return changed
15✔
993
end
994

995
function edit_shift_move(s::MIState, move_function::Function)
12✔
996
    @assert command_group(move_function) === :movement
12✔
997
    set_action!(s, Symbol(:shift_, move_function))
12✔
998
    return move_function(s)
12✔
999
end
1000

1001

1002
# splice! for IOBuffer: convert from close-open region to index, update the size,
1003
# and keep the cursor position and mark stable with the text
1004
# returns the removed portion as a String
1005
function edit_splice!(s::BufferLike, r::Region=region(s), ins::String = ""; rigid_mark::Bool=true)
1,605✔
1006
    A, B = first(r), last(r)
642✔
1007
    A >= B && isempty(ins) && return ins
642✔
1008
    buf = buffer(s)
744✔
1009
    pos = position(buf) # n.b. position(), etc, are 0-indexed
636✔
1010
    adjust_pos = true
636✔
1011
    if A <= pos < B
636✔
1012
        seek(buf, A)
414✔
1013
    elseif B <= pos
429✔
1014
        seek(buf, pos - B + A)
369✔
1015
    else
1016
        adjust_pos = false
60✔
1017
    end
1018
    mark = buf.mark
636✔
1019
    if mark != -1
636✔
1020
        if A < mark < B || A == mark == B
234✔
1021
            # rigid_mark is used only if the mark is strictly "inside"
1022
            # the region, or the region is empty and the mark is at the boundary
1023
            mark = rigid_mark ? A : A + sizeof(ins)
27✔
1024
        elseif mark >= B
96✔
1025
            mark += sizeof(ins) - B + A
21✔
1026
        end
1027
        buf.mark = -1
120✔
1028
    end
1029
    # Implement ret = splice!(buf.data, A+1:B, codeunits(ins)) for a stream
1030
    pos = position(buf)
636✔
1031
    seek(buf, A)
1,272✔
1032
    ret = read(buf, A >= B ? 0 : B - A)
636✔
1033
    trail = read(buf)
636✔
1034
    seek(buf, A)
1,272✔
1035
    write(buf, ins)
636✔
1036
    write(buf, trail)
636✔
1037
    truncate(buf, position(buf))
636✔
1038
    seek(buf, pos + (adjust_pos ? sizeof(ins) : 0))
1,272✔
1039
    buf.mark = mark
636✔
1040
    return String(ret)
636✔
1041
end
1042

1043
edit_splice!(s::MIState, ins::AbstractString) = edit_splice!(s, region(s), ins)
9✔
1044

1045
function edit_insert(s::PromptState, c::StringLike)
8,077✔
1046
    push_undo(s)
8,077✔
1047
    buf = s.input_buffer
8,077✔
1048

1049
    if ! options(s).auto_indent_bracketed_paste
8,239✔
1050
        pos = position(buf)
5,080✔
1051
        if pos > 0
5,080✔
1052
            if buf.data[pos] != _space && string(c) != " "
9,044✔
1053
                options(s).auto_indent_tmp_off = false
4,093✔
1054
            end
1055
            if buf.data[pos] == _space
4,792✔
1056
                #tabulators are already expanded to space
1057
                #this expansion may take longer than auto_indent_time_threshold which breaks the timing
1058
                s.last_newline = time()
411✔
1059
            else
1060
                #if characters after new line are coming in very fast
1061
                #its probably copy&paste => switch auto-indent off for the next coming new line
1062
                if ! options(s).auto_indent_tmp_off && time() - s.last_newline < options(s).auto_indent_time_threshold
4,453✔
1063
                    options(s).auto_indent_tmp_off = true
2,819✔
1064
                end
1065
            end
1066
        end
1067
    end
1068

1069
    old_wait = s.refresh_wait !== nothing
8,077✔
1070
    if old_wait
8,077✔
UNCOV
1071
        close(s.refresh_wait)
×
1072
        s.refresh_wait = nothing
×
1073
    end
1074
    str = string(c)
16,001✔
1075
    edit_insert(buf, str)
8,134✔
1076
    if '\n' in str
8,077✔
1077
        refresh_line(s)
33✔
1078
    else
1079
        after = options(s).auto_refresh_time_delay
8,173✔
1080
        termbuf = terminal(s)
8,044✔
1081
        w = width(termbuf)
8,044✔
1082
        offset = s.ias.curs_row == 1 || s.indent < 0 ?
9,951✔
1083
            sizeof(prompt_string(s.p.prompt)::String) : s.indent
1084
        offset += position(buf) - beginofline(buf) # size of current line
8,332✔
1085
        spinner = '\0'
8,044✔
1086
        delayup = !eof(buf) || old_wait
8,044✔
1087
        # Disable fast path when syntax highlighting is enabled
1088
        use_fast_path = offset + textwidth(str) <= w && !(after == 0 && delayup) && !options(s).style_input
8,161✔
1089
        if use_fast_path
8,044✔
1090
            # Avoid full update when appending characters to the end
1091
            # and an update of curs_row isn't necessary (conservatively estimated)
1092
            write(termbuf, str)
5,799✔
1093
            spinner = ' ' # temporarily clear under the cursor
5,799✔
1094
        elseif after == 0
2,245✔
1095
            refresh_line(s)
2,245✔
1096
            delayup = false
2,245✔
1097
        else # render a spinner for each key press
UNCOV
1098
            if old_wait || length(str) != 1
×
UNCOV
1099
                spinner = spin_seq[mod1(position(buf) - w, length(spin_seq))]
×
1100
            else
UNCOV
1101
                spinner = str[end]
×
1102
            end
UNCOV
1103
            delayup = true
×
1104
        end
1105
        if delayup
8,044✔
UNCOV
1106
            if spinner != '\0'
×
UNCOV
1107
                write(termbuf, spinner)
×
UNCOV
1108
                cmove_left(termbuf)
×
1109
            end
UNCOV
1110
            s.refresh_wait = Timer(after) do t
×
1111
                s.refresh_wait === t || return
1112
                s.refresh_wait = nothing
1113
                refresh_line(s)
1114
            end
1115
        end
1116
    end
1117
    nothing
8,077✔
1118
end
1119
const spin_seq = ("⋯", "⋱", "⋮", "⋰")
1120

1121
function edit_insert(buf::IOBuffer, c::StringLike)
51✔
1122
    if eof(buf)
8,416✔
1123
        return write(buf, c)
8,272✔
1124
    else
1125
        s = string(c)
144✔
1126
        edit_splice!(buf, position(buf) => position(buf), s)
144✔
1127
        return sizeof(s)
144✔
1128
    end
1129
end
1130

1131
# align: number of ' ' to insert after '\n'
1132
# if align < 0: align like line above
1133
function edit_insert_newline(s::PromptState, align::Int = 0 - options(s).auto_indent)
51✔
1134
    push_undo(s)
123✔
1135
    buf = buffer(s)
51✔
1136
    autoindent = align < 0
51✔
1137
    if autoindent && ! options(s).auto_indent_tmp_off
78✔
1138
        beg = beginofline(buf)
51✔
1139
        align = min(something(findnext(_notspace, buf.data[beg+1:buf.size], 1), 0) - 1,
51✔
1140
                    position(buf) - beg) # indentation must not increase
1141
        align < 0 && (align = buf.size-beg)
27✔
1142
    #else
1143
    #    align = 0
1144
    end
1145
    align < 0 && (align = 0)
51✔
1146
    edit_insert(buf, '\n' * ' '^align)
66✔
1147
    refresh_line(s)
51✔
1148
    # updating s.last_newline should happen after refresh_line(s) which can take
1149
    # an unpredictable amount of time and makes "paste detection" unreliable
1150
    if ! options(s).auto_indent_bracketed_paste
84✔
1151
        s.last_newline = time()
51✔
1152
    end
1153
    nothing
51✔
1154
end
1155

1156
# align: delete up to 4 spaces to align to a multiple of 4 chars
1157
# adjust: also delete spaces on the right of the cursor to try to keep aligned what is
1158
# on the right
1159
function edit_backspace(s::PromptState, align::Bool=options(s).backspace_align,
114✔
1160
                        adjust::Bool=options(s).backspace_adjust)
1161
    push_undo(s)
213✔
1162
    if edit_backspace(buffer(s), align, adjust)
114✔
1163
        return refresh_line(s)
114✔
1164
    else
UNCOV
1165
        pop_undo(s)
×
UNCOV
1166
        return beep(s)
×
1167
    end
1168
end
1169

1170
const _newline =  UInt8('\n')
1171
const _space = UInt8(' ')
1172

1173
_notspace(c) = c != _space
489✔
1174

1175
beginofline(buf::IOBuffer, pos::Int=position(buf)) = something(findprev(isequal(_newline), buf.data, pos), 0)
17,141✔
1176

1177
function endofline(buf::IOBuffer, pos::Int=position(buf))
1178
    eol = findnext(isequal(_newline), buf.data[pos+1:buf.size], 1)
219✔
1179
    eol === nothing ? buf.size : pos + eol - 1
150✔
1180
end
1181

1182
function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=false)
123✔
1183
    !align && adjust &&
129✔
1184
        throw(DomainError((align, adjust),
1185
                          "if `adjust` is `true`, `align` must be `true`"))
1186
    oldpos = position(buf)
123✔
1187
    oldpos == 0 && return false
123✔
1188
    c = char_move_left(buf)
123✔
1189
    newpos = position(buf)
123✔
1190
    if align && c == ' ' # maybe delete multiple spaces
123✔
1191
        beg = beginofline(buf, newpos)
36✔
1192
        align = textwidth(String(buf.data[1+beg:newpos])) % 4
36✔
1193
        nonspace = something(findprev(_notspace, buf.data, newpos), 0)
36✔
1194
        if newpos - align >= nonspace
18✔
1195
            newpos -= align
18✔
1196
            seek(buf, newpos)
36✔
1197
            if adjust
18✔
1198
                spaces = something(findnext(_notspace, buf.data[newpos+2:buf.size], 1), 0)
12✔
1199
                oldpos = spaces == 0 ? buf.size :
12✔
1200
                    buf.data[newpos+1+spaces] == _newline ? newpos+spaces :
1201
                    newpos + min(spaces, 4)
1202
            end
1203
        end
1204
    end
1205
    edit_splice!(buf, newpos => oldpos)
123✔
1206
    return true
123✔
1207
end
1208

1209
function edit_delete(s::MIState)
3✔
1210
    set_action!(s, :edit_delete)
3✔
1211
    push_undo(s)
3✔
1212
    if edit_delete(buffer(s))
6✔
1213
        return refresh_line(s)
3✔
1214
    else
UNCOV
1215
        pop_undo(s)
×
UNCOV
1216
        return beep(s)
×
1217
    end
1218
end
1219

1220
function edit_delete(buf::IOBuffer)
1221
    eof(buf) && return false
9✔
1222
    oldpos = position(buf)
9✔
1223
    char_move_right(buf)
9✔
1224
    edit_splice!(buf, oldpos => position(buf))
9✔
1225
    return true
9✔
1226
end
1227

1228
function edit_werase(buf::IOBuffer)
1229
    pos1 = position(buf)
9✔
1230
    char_move_word_left(buf, isspace)
9✔
1231
    pos0 = position(buf)
9✔
1232
    return edit_splice!(buf, pos0 => pos1)
9✔
1233
end
1234

1235
function edit_werase(s::MIState)
9✔
1236
    set_action!(s, :edit_werase)
9✔
1237
    push_undo(s)
9✔
1238
    if push_kill!(s, edit_werase(buffer(s)), rev=true)
9✔
1239
        return refresh_line(s)
9✔
1240
    else
UNCOV
1241
        pop_undo(s)
×
UNCOV
1242
        return :ignore
×
1243
    end
1244
end
1245

1246
function edit_delete_prev_word(buf::IOBuffer)
15✔
1247
    pos1 = position(buf)
24✔
1248
    char_move_word_left(buf)
24✔
1249
    pos0 = position(buf)
24✔
1250
    return edit_splice!(buf, pos0 => pos1)
24✔
1251
end
1252

1253
function edit_delete_prev_word(s::MIState)
9✔
1254
    set_action!(s, :edit_delete_prev_word)
9✔
1255
    push_undo(s)
9✔
1256
    if push_kill!(s, edit_delete_prev_word(buffer(s)), rev=true)
9✔
1257
        return refresh_line(s)
9✔
1258
    else
UNCOV
1259
        pop_undo(s)
×
UNCOV
1260
        return :ignore
×
1261
    end
1262
end
1263

1264
function edit_delete_next_word(buf::IOBuffer)
1265
    pos0 = position(buf)
9✔
1266
    char_move_word_right(buf)
9✔
1267
    pos1 = position(buf)
9✔
1268
    return edit_splice!(buf, pos0 => pos1)
9✔
1269
end
1270

1271
function edit_delete_next_word(s::MIState)
9✔
1272
    set_action!(s, :edit_delete_next_word)
9✔
1273
    push_undo(s)
9✔
1274
    if push_kill!(s, edit_delete_next_word(buffer(s)))
9✔
1275
        return refresh_line(s)
9✔
1276
    else
UNCOV
1277
        pop_undo(s)
×
UNCOV
1278
        return :ignore
×
1279
    end
1280
end
1281

1282
function edit_yank(s::MIState)
27✔
1283
    set_action!(s, :edit_yank)
27✔
1284
    if isempty(s.kill_ring)
27✔
UNCOV
1285
        beep(s)
×
UNCOV
1286
        return :ignore
×
1287
    end
1288
    setmark(s) # necessary for edit_yank_pop
27✔
1289
    push_undo(s)
27✔
1290
    edit_insert(buffer(s), s.kill_ring[mod1(s.kill_idx, end)])
36✔
1291
    return refresh_line(s)
27✔
1292
end
1293

1294
function edit_yank_pop(s::MIState, require_previous_yank::Bool=true)
12✔
1295
    set_action!(s, :edit_yank_pop)
21✔
1296
    repeat = s.last_action ∈ (:edit_yank, :edit_yank_pop)
12✔
1297
    if require_previous_yank && !repeat || isempty(s.kill_ring)
21✔
1298
        beep(s)
3✔
1299
        return :ignore
3✔
1300
    else
1301
        require_previous_yank || repeat || setmark(s)
12✔
1302
        push_undo(s)
9✔
1303
        edit_splice!(s, s.kill_ring[mod1(s.kill_idx -= 1, end)])
9✔
1304
        return refresh_line(s)
9✔
1305
    end
1306
end
1307

1308
function push_kill!(s::MIState, killed::String, concat::Bool = s.key_repeats > 0; rev::Bool=false)
318✔
1309
    isempty(killed) && return false
117✔
1310
    if concat && !isempty(s.kill_ring)
111✔
1311
        s.kill_ring[end] = rev ?
18✔
1312
            killed * s.kill_ring[end] : # keep expected order for backward deletion
1313
            s.kill_ring[end] * killed
1314
    else
1315
        push!(s.kill_ring, killed)
93✔
1316
        length(s.kill_ring) > options(s).kill_ring_max && popfirst!(s.kill_ring)
93✔
1317
    end
1318
    s.kill_idx = lastindex(s.kill_ring)
111✔
1319
    return true
111✔
1320
end
1321

1322
function edit_kill_line(s::MIState, backwards::Bool=false)
78✔
1323
    buf = buffer(s)
81✔
1324
    if backwards
57✔
1325
        set_action!(s, :edit_kill_line_backwards)
33✔
1326
        pos = beginofline(buf)
42✔
1327
        endpos = position(buf)
33✔
1328
        pos == endpos && pos > 0 && (pos -= 1)
33✔
1329
    else
1330
        set_action!(s, :edit_kill_line_forwards)
24✔
1331
        pos = position(buf)
24✔
1332
        endpos = endofline(buf)
30✔
1333
        endpos == pos && buf.size > pos && (endpos += 1)
24✔
1334
    end
1335
    push_undo(s)
57✔
1336
    if push_kill!(s, edit_splice!(s, pos => endpos); rev=backwards)
57✔
1337
        return refresh_line(s)
54✔
1338
    else
1339
        pop_undo(s)
3✔
1340
        beep(s)
3✔
1341
        return :ignore
3✔
1342
    end
1343
end
1344

UNCOV
1345
edit_kill_line_forwards(s::MIState) = edit_kill_line(s, false)
×
1346
edit_kill_line_backwards(s::MIState) = edit_kill_line(s, true)
33✔
1347

1348
function edit_copy_region(s::MIState)
3✔
1349
    set_action!(s, :edit_copy_region)
3✔
1350
    buf = buffer(s)
3✔
1351
    push_kill!(s, content(buf, region(buf)), false) || return :ignore
3✔
1352
    if options(s).region_animation_duration > 0.0
6✔
1353
        edit_exchange_point_and_mark(s)
3✔
1354
        sleep(options(s).region_animation_duration)
3✔
1355
        edit_exchange_point_and_mark(s)
3✔
1356
    end
1357
    nothing
3✔
1358
end
1359

1360
function edit_kill_region(s::MIState)
6✔
1361
    set_action!(s, :edit_kill_region)
6✔
1362
    push_undo(s)
6✔
1363
    if push_kill!(s, edit_splice!(s), false)
6✔
1364
        return refresh_line(s)
6✔
1365
    else
UNCOV
1366
        pop_undo(s)
×
UNCOV
1367
        return :ignore
×
1368
    end
1369
end
1370

1371
function edit_transpose_chars(s::MIState)
3✔
1372
    set_action!(s, :edit_transpose_chars)
3✔
1373
    push_undo(s)
3✔
1374
    return edit_transpose_chars(buffer(s)) ? refresh_line(s) : pop_undo(s)
3✔
1375
end
1376

1377
function edit_transpose_chars(buf::IOBuffer)
42✔
1378
    # Moving left but not transpoing anything is intentional, and matches Emacs's behavior
1379
    eof(buf) && position(buf) !== 0 && char_move_left(buf)
42✔
1380
    position(buf) == 0 && return false
42✔
1381
    char_move_left(buf)
27✔
1382
    pos = position(buf)
27✔
1383
    a, b = read(buf, Char), read(buf, Char)
27✔
1384
    seek(buf, pos)
54✔
1385
    write(buf, b, a)
27✔
1386
    return true
27✔
1387
end
1388

1389
function edit_transpose_words(s::MIState)
3✔
1390
    set_action!(s, :edit_transpose_words)
3✔
1391
    push_undo(s)
3✔
1392
    return edit_transpose_words(buffer(s)) ? refresh_line(s) : pop_undo(s)
3✔
1393
end
1394

1395
function edit_transpose_words(buf::IOBuffer, mode::Symbol=:emacs)
51✔
1396
    mode in [:readline, :emacs] ||
54✔
1397
        throw(ArgumentError("`mode` must be `:readline` or `:emacs`"))
1398
    pos = position(buf)
51✔
1399
    if mode === :emacs
51✔
1400
        char_move_word_left(buf)
18✔
1401
        char_move_word_right(buf)
18✔
1402
    end
1403
    char_move_word_right(buf)
51✔
1404
    e2 = position(buf)
51✔
1405
    char_move_word_left(buf)
51✔
1406
    b2 = position(buf)
51✔
1407
    char_move_word_left(buf)
51✔
1408
    b1 = position(buf)
51✔
1409
    char_move_word_right(buf)
51✔
1410
    e1 = position(buf)
51✔
1411
    e1 >= b2 && (seek(buf, pos); return false)
51✔
1412
    word2 = edit_splice!(buf, b2 => e2, content(buf, b1 => e1))
42✔
1413
    edit_splice!(buf, b1 => e1, word2)
42✔
1414
    seek(buf, e2)
84✔
1415
    return true
42✔
1416
end
1417

1418

1419
# swap all lines intersecting the region with line above
1420
function edit_transpose_lines_up!(buf::IOBuffer, reg::Region)
18✔
1421
    b2 = beginofline(buf, first(reg))
24✔
1422
    b2 == 0 && return false
18✔
1423
    b1 = beginofline(buf, b2-1)
9✔
1424
    # we do in this order so that the buffer's position is maintained in current line
1425
    line1 = edit_splice!(buf, b1 => b2) # delete whole previous line
6✔
1426
    line1 = '\n'*line1[1:end-1] # don't include the final '\n'
12✔
1427
    pos = position(buf) # save pos in case it's at the end of line
6✔
1428
    b = endofline(buf, last(reg) - b2 + b1) # b2-b1 is the size of the removed line1
9✔
1429
    edit_splice!(buf, b => b, line1)
6✔
1430
    seek(buf, pos)
12✔
1431
    return true
6✔
1432
end
1433

1434
# swap all lines intersecting the region with line below
1435
function edit_transpose_lines_down!(buf::IOBuffer, reg::Region)
18✔
1436
    e1 = endofline(buf, last(reg))
33✔
1437
    e1 == buf.size && return false
18✔
1438
    e2 = endofline(buf, e1+1)
24✔
1439
    line2 = edit_splice!(buf, e1 => e2) # delete whole next line
15✔
1440
    line2 = line2[2:end]*'\n' # don't include leading '\n'
30✔
1441
    b = beginofline(buf, first(reg))
18✔
1442
    edit_splice!(buf, b => b, line2, rigid_mark=false)
15✔
1443
    return true
15✔
1444
end
1445

1446
# return the region if active, or the current position as a Region otherwise
UNCOV
1447
region_if_active(s::MIState)::Region = is_region_active(s) ? region(s) : position(s)=>position(s)
×
1448

UNCOV
1449
function edit_transpose_lines_up!(s::MIState)
×
UNCOV
1450
    set_action!(s, :edit_transpose_lines_up!)
×
UNCOV
1451
    if edit_transpose_lines_up!(buffer(s), region_if_active(s))
×
UNCOV
1452
        return refresh_line(s)
×
1453
    else
1454
        # beeping would be too noisy here
UNCOV
1455
        return :ignore
×
1456
    end
1457
end
1458

UNCOV
1459
function edit_transpose_lines_down!(s::MIState)
×
UNCOV
1460
    set_action!(s, :edit_transpose_lines_down!)
×
UNCOV
1461
    if edit_transpose_lines_down!(buffer(s), region_if_active(s))
×
UNCOV
1462
        return refresh_line(s)
×
1463
    else
UNCOV
1464
        return :ignore
×
1465
    end
1466
end
1467

1468
function edit_upper_case(s::BufferLike)
3✔
1469
    set_action!(s, :edit_upper_case)
6✔
1470
    return edit_replace_word_right(s, uppercase)
6✔
1471
end
1472
function edit_lower_case(s::BufferLike)
3✔
1473
    set_action!(s, :edit_lower_case)
6✔
1474
    return edit_replace_word_right(s, lowercase)
6✔
1475
end
1476
function edit_title_case(s::BufferLike)
3✔
1477
    set_action!(s, :edit_title_case)
6✔
1478
    return edit_replace_word_right(s, titlecase)
6✔
1479
end
1480

1481
function edit_replace_word_right(s::Union{MIState,ModeState}, replace::Function)
9✔
1482
    push_undo(s)
9✔
1483
    return edit_replace_word_right(buffer(s), replace) ? refresh_line(s) : pop_undo(s)
9✔
1484
end
1485

1486
function edit_replace_word_right(buf::IOBuffer, replace::Function)
18✔
1487
    # put the cursor at the beginning of the next word
1488
    skipchars(is_non_word_char, buf)
18✔
1489
    b = position(buf)
18✔
1490
    char_move_word_right(buf)
18✔
1491
    e = position(buf)
18✔
1492
    e == b && return false
18✔
1493
    edit_splice!(buf, b => e, replace(content(buf, b => e)))
18✔
1494
    return true
18✔
1495
end
1496

1497
edit_clear(buf::IOBuffer) = truncate(buf, 0)
9✔
1498

1499
function edit_clear(s::MIState)
24✔
1500
    set_action!(s, :edit_clear)
24✔
1501
    push_undo(s)
24✔
1502
    if push_kill!(s, edit_splice!(s, 0 => bufend(s)), false)
24✔
1503
        return refresh_line(s)
21✔
1504
    else
1505
        pop_undo(s)
3✔
1506
        return :ignore
3✔
1507
    end
1508
end
1509

1510
function replace_line(s::PromptState, l::IOBuffer)
51✔
1511
    empty_undo(s)
51✔
1512
    s.input_buffer = copy(l)
51✔
1513
    deactivate_region(s)
51✔
1514
    nothing
51✔
1515
end
1516

1517
function replace_line(s::PromptState, l::Union{String,SubString{String}}, keep_undo::Bool=false)
267✔
1518
    keep_undo || empty_undo(s)
393✔
1519
    s.input_buffer.ptr = 1
141✔
1520
    s.input_buffer.size = 0
141✔
1521
    write(s.input_buffer, l)
282✔
1522
    deactivate_region(s)
141✔
1523
    nothing
141✔
1524
end
1525

1526

UNCOV
1527
edit_indent_left(s::MIState, n=1) = edit_indent(s, -n)
×
1528
edit_indent_right(s::MIState, n=1) = edit_indent(s, n)
3✔
1529

1530
function edit_indent(s::MIState, num::Int)
3✔
1531
    set_action!(s, :edit_indent)
3✔
1532
    push_undo(s)
3✔
1533
    if edit_indent(buffer(s), num, is_region_active(s))
3✔
1534
        return refresh_line(s)
3✔
1535
    else
UNCOV
1536
        pop_undo(s)
×
UNCOV
1537
        return :ignore
×
1538
    end
1539
end
1540

1541
# return the indices in buffer(s) of the beginning of each lines
1542
# having a non-empty intersection with region(s)
1543
function get_lines_in_region(s::BufferLike)
12✔
1544
    buf = buffer(s)
12✔
1545
    b, e = region(buf)
12✔
1546
    bol = Int[beginofline(buf, b)] # begin of lines
15✔
1547
    while true
33✔
1548
        b = endofline(buf, b)
54✔
1549
        b >= e && break
33✔
1550
        # b < e ==> b+1 <= e <= buf.size
1551
        push!(bol, b += 1)
21✔
1552
    end
21✔
1553
    return bol
12✔
1554
end
1555

1556
# compute the number of spaces from b till the next non-space on the right
1557
# (which can also be "end of line" or "end of buffer")
1558
function leadingspaces(buf::IOBuffer, b::Int)
1559
    @views ls = something(findnext(_notspace, buf.data[1:buf.size], b+1), 0)-1
102✔
1560
    ls == -1 && (ls = buf.size)
51✔
1561
    ls -= b
51✔
1562
    return ls
51✔
1563
end
1564

1565
# indent by abs(num) characters, on the right if num >= 0, on the left otherwise
1566
# if multiline is true, indent all the lines in the region as a block.
1567
function edit_indent(buf::IOBuffer, num::Int, multiline::Bool)
57✔
1568
    bol = multiline ? get_lines_in_region(buf) : Int[beginofline(buf)]
102✔
1569
    if num < 0
57✔
1570
        # count leading spaces on the lines, which are an upper bound
1571
        # on the number of spaces characters that can be removed
1572
        ls_min = minimum(leadingspaces(buf, b) for b in bol)
36✔
1573
        ls_min == 0 && return false # can't left-indent, no space can be removed
36✔
1574
        num = -min(-num, ls_min)
24✔
1575
    end
1576
    for b in reverse!(bol) # reverse! to not mess-up the bol's offsets
45✔
1577
        _edit_indent(buf, b, num)
93✔
1578
    end
60✔
1579
    return true
45✔
1580
end
1581

1582
# indents line starting a position b by num positions
1583
# if num < 0, it is assumed that there are at least num white spaces
1584
# at the beginning of line
1585
_edit_indent(buf::IOBuffer, b::Int, num::Int) =
93✔
1586
    num >= 0 ? edit_splice!(buf, b => b, ' '^num, rigid_mark=false) :
1587
               edit_splice!(buf, b => (b - num))
1588

1589
function mode_idx(hist::HistoryProvider, mode::TextInterface)
466✔
1590
    c = :julia
466✔
1591
    for (k,v) in hist.mode_mapping
466✔
1592
        isequal(v, mode) && (c = k)
1,681✔
1593
    end
1,681✔
1594
    return c
466✔
1595
end
1596

1597
function guess_current_mode_name(s)
3✔
1598
    try
3✔
1599
        mode_idx(s.current_mode.hist, s.current_mode)
3✔
1600
    catch
UNCOV
1601
        nothing
×
1602
    end
1603
end
1604

1605
# edit current input in editor
1606
function edit_input(s, f = (filename, line, column) -> InteractiveUtils.edit(filename, line, column))
3✔
1607
    mode_name = guess_current_mode_name(s)
3✔
1608
    filename = tempname()
3✔
1609
    if mode_name === :julia
3✔
1610
        filename *= ".jl"
3✔
UNCOV
1611
    elseif mode_name === :shell
×
UNCOV
1612
        filename *= ".sh"
×
1613
    end
1614
    buf = buffer(s)
3✔
1615
    pos = position(buf)
3✔
1616
    str = takestring!(buf)
3✔
1617
    lines = readlines(IOBuffer(str); keep=true)
3✔
1618

1619
    # Compute line
1620
    line_start_offset = 0
3✔
1621
    line = 1
3✔
1622
    while line < length(lines) && line_start_offset + sizeof(lines[line]) <= pos
3✔
UNCOV
1623
        line_start_offset += sizeof(lines[line])
×
1624
        line += 1
×
1625
    end
×
1626

1627
    # Compute column
1628
    col = 0
3✔
1629
    off = line_start_offset
3✔
1630
    while off <= pos
24✔
1631
        off = nextind(str, off)
39✔
1632
        col += 1
21✔
1633
    end
21✔
1634

1635
    # Write current input to temp file, edit, read back
1636
    write(filename, str)
3✔
1637
    f(filename, line, col)
3✔
1638
    str_mod = readchomp(filename)
3✔
1639
    rm(filename)
3✔
1640

1641
    # Write updated content
1642
    write(buf, str_mod)
3✔
1643
    if str == str_mod
3✔
1644
        # If input was not modified: reset cursor
UNCOV
1645
        seek(buf, pos)
×
1646
    else
1647
        # If input was modified: move cursor to end
1648
        move_input_end(s)
3✔
1649
    end
1650
    refresh_line(s)
3✔
1651
end
1652

1653
# return the identifier under the cursor, possibly with other words concatenated
1654
# to it with dots (e.g. "A.B.C" in "X; A.B.C*3", if the cursor is between "A" and "C")
1655
function current_word_with_dots(buf::IOBuffer)
×
UNCOV
1656
    pos = position(buf)
×
UNCOV
1657
    while true
×
1658
        char_move_word_right(buf)
×
1659
        if eof(buf) || peek(buf, Char) != '.'
×
1660
            break
×
1661
        end
1662
    end
×
1663
    pend = position(buf)
×
UNCOV
1664
    while true
×
UNCOV
1665
        char_move_word_left(buf)
×
1666
        p = position(buf)
×
UNCOV
1667
        p == 0 && break
×
1668
        seek(buf, p-1)
×
1669
        if peek(buf, Char) != '.'
×
UNCOV
1670
            seek(buf, p)
×
UNCOV
1671
            break
×
1672
        end
1673
    end
×
1674
    pbegin = position(buf)
×
1675
    word = pend > pbegin ?
×
1676
        String(buf.data[pbegin+1:pend]) :
1677
        ""
UNCOV
1678
    seek(buf, pos)
×
1679
    word
×
1680
end
1681

1682
current_word_with_dots(s::MIState) = current_word_with_dots(buffer(s))
×
1683

1684
function activate_module(s::MIState)
×
1685
    word = current_word_with_dots(s);
×
1686
    empty = isempty(word)
×
1687
    mod = if empty
×
UNCOV
1688
        s.previous_active_module
×
1689
    else
UNCOV
1690
        try
×
1691
            Base.Core.eval(Base.active_module(), Base.Meta.parse(word))
×
1692
        catch
1693
            nothing
×
1694
        end
1695
    end
1696
    if !(mod isa Module) || mod == Base.active_module()
×
1697
        beep(s)
×
1698
        return
×
1699
    end
1700
    empty && edit_insert(s, ' ') # makes the `edit_clear` below actually update the prompt
×
UNCOV
1701
    if Base.active_module() == Main || mod == Main
×
1702
        # At least one needs to be Main. Disallows toggling between two non-Main modules because it's
1703
        # otherwise hard to get back to Main
UNCOV
1704
        s.previous_active_module = Base.active_module()
×
1705
    end
UNCOV
1706
    REPL.activate(mod)
×
UNCOV
1707
    edit_clear(s)
×
1708
end
1709

UNCOV
1710
history_prev(::EmptyHistoryProvider) = ("", false)
×
UNCOV
1711
history_next(::EmptyHistoryProvider) = ("", false)
×
UNCOV
1712
history_first(::EmptyHistoryProvider) = ("", false)
×
UNCOV
1713
history_last(::EmptyHistoryProvider) = ("", false)
×
UNCOV
1714
history_search(::EmptyHistoryProvider, args...) = false
×
UNCOV
1715
add_history(::EmptyHistoryProvider, s) = nothing
×
1716
add_history(s::PromptState) = add_history(mode(s).hist, s)
360✔
UNCOV
1717
history_next_prefix(s, hist, prefix) = false
×
UNCOV
1718
history_prev_prefix(s, hist, prefix) = false
×
1719

UNCOV
1720
function history_prev(s::ModeState, hist)
×
UNCOV
1721
    l, ok = history_prev(mode(s).hist)
×
UNCOV
1722
    if ok
×
UNCOV
1723
        replace_line(s, l)
×
UNCOV
1724
        move_input_start(s)
×
UNCOV
1725
        refresh_line(s)
×
1726
    else
UNCOV
1727
        beep(s)
×
1728
    end
UNCOV
1729
    nothing
×
1730
end
UNCOV
1731
function history_next(s::ModeState, hist)
×
UNCOV
1732
    l, ok = history_next(mode(s).hist)
×
UNCOV
1733
    if ok
×
UNCOV
1734
        replace_line(s, l)
×
UNCOV
1735
        move_input_end(s)
×
UNCOV
1736
        refresh_line(s)
×
1737
    else
UNCOV
1738
        beep(s)
×
1739
    end
UNCOV
1740
    nothing
×
1741
end
1742

1743
refresh_line(s::BufferLike) = refresh_multi_line(s)
3,821✔
1744
refresh_line(s::BufferLike, termbuf::AbstractTerminal) = refresh_multi_line(termbuf, s)
1,137✔
1745

UNCOV
1746
default_completion_cb(::IOBuffer) = []
×
UNCOV
1747
default_enter_cb(_) = true
×
1748

1749
write_prompt(terminal::AbstractTerminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color)
4,763✔
1750
function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool)
4,964✔
1751
    prefix = prompt_string(p.prompt_prefix)
9,928✔
1752
    suffix = prompt_string(p.prompt_suffix)
9,923✔
1753
    write(terminal, prefix)
4,964✔
1754
    color && write(terminal, Base.text_colors[:bold])
4,964✔
1755
    width = write_prompt(terminal, p.prompt, color)
7,321✔
1756
    color && write(terminal, Base.text_colors[:normal])
4,964✔
1757
    write(terminal, suffix)
4,964✔
1758
    return width
4,964✔
1759
end
1760

1761
function write_output_prefix(io::IO, p::Prompt, color::Bool)
183✔
1762
    prefix = prompt_string(p.output_prefix_prefix)
366✔
1763
    suffix = prompt_string(p.output_prefix_suffix)
366✔
1764
    print(io, prefix)
183✔
1765
    color && write(io, Base.text_colors[:bold])
183✔
1766
    width = write_prompt(io, p.output_prefix, color)
324✔
1767
    color && write(io, Base.text_colors[:normal])
183✔
1768
    print(io, suffix)
183✔
1769
    return width
183✔
1770
end
1771

1772
# On Windows, when launching external processes, we cannot control what assumption they make on the
1773
# console mode. We thus forcibly reset the console mode at the start of the prompt to ensure they do
1774
# not leave the console mode in a corrupt state.
1775
# FIXME: remove when pseudo-tty are implemented for child processes
1776
if Sys.iswindows()
1777

1778
#= Get/SetConsoleMode flags =#
1779
const ENABLE_PROCESSED_OUTPUT            = UInt32(0x0001)
1780
const ENABLE_WRAP_AT_EOL_OUTPUT          = UInt32(0x0002)
1781
const ENABLE_VIRTUAL_TERMINAL_PROCESSING = UInt32(0x0004)
1782
const DISABLE_NEWLINE_AUTO_RETURN        = UInt32(0x0008)
1783
const ENABLE_LVB_GRID_WORLDWIDE          = UInt32(0x0010)
1784

1785
#= libuv flags =#
1786
const UV_TTY_SUPPORTED = 0
1787
const UV_TTY_UNSUPPORTED = 1
1788

1789
function _reset_console_mode(handle::Ptr{Cvoid})
1790
    # Query libuv to see whether it expects the console to support virtual terminal sequences
UNCOV
1791
    vterm_state = Ref{Cint}()
×
UNCOV
1792
    ccall(:uv_tty_get_vterm_state, Cint, (Ref{Cint},), vterm_state)
×
1793

UNCOV
1794
    mode::UInt32 = ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT
×
UNCOV
1795
    if vterm_state[] == UV_TTY_SUPPORTED
×
UNCOV
1796
        mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
×
1797
    end
1798

1799
    # Expected to fail (benignly) with ERROR_INVALID_HANDLE if the provided handle does not
1800
    # allow setting the console mode
1801
    ccall(:SetConsoleMode, stdcall, Int32, (Ptr{Cvoid}, UInt32), handle, mode)
×
1802

1803
    return nothing
×
1804
end
1805

1806
end
1807

1808
# returns the width of the written prompt
1809
function write_prompt(terminal::Union{IO, AbstractTerminal}, s::Union{AbstractString,Function}, color::Bool)
5,150✔
1810
    promptstr = prompt_string(s)::String
7,799✔
1811
    write(terminal, promptstr)
5,150✔
1812
    return textwidth(promptstr)
5,150✔
1813
end
1814

1815
### Keymap Support
1816

1817
const wildcard = '\U10f7ff' # "Private Use" Char
1818

1819
normalize_key(key::Char) = string(key)
6,516✔
1820
normalize_key(key::Union{Int,UInt8}) = normalize_key(Char(key))
170✔
1821
function normalize_key(key::Union{String,SubString{String}})
35,304✔
1822
    wildcard in key && error("Matching '\U10f7ff' not supported.")
70,608✔
1823
    buf = IOBuffer()
35,304✔
1824
    i = firstindex(key)
35,304✔
1825
    while i <= ncodeunits(key)
287,904✔
1826
        c, i = iterate(key, i)
217,296✔
1827
        if c == '*'
108,648✔
1828
            write(buf, wildcard)
8,436✔
1829
        elseif c == '^'
100,212✔
1830
            c, i = iterate(key, i)
9,284✔
1831
            write(buf, uppercase(c)-64)
4,642✔
1832
        elseif c == '\\'
95,570✔
1833
            c, i = iterate(key, i)
6✔
1834
            if c == 'C'
3✔
1835
                c, i = iterate(key, i)
6✔
1836
                c == '-' || error("the Control key specifier must start with \"\\\\C-\"")
3✔
1837
                c, i = iterate(key, i)
6✔
1838
                write(buf, uppercase(c)-64)
3✔
UNCOV
1839
            elseif c == 'M'
×
UNCOV
1840
                c, i = iterate(key, i)
×
UNCOV
1841
                c == '-' || error("the Meta key specifier must start with \"\\\\M-\"")
×
UNCOV
1842
                c, i = iterate(key, i)
×
UNCOV
1843
                write(buf, '\e')
×
UNCOV
1844
                write(buf, c)
×
UNCOV
1845
            elseif c == '^'
×
UNCOV
1846
                write(buf, c)
×
1847
            end
1848
        else
1849
            write(buf, c)
95,567✔
1850
        end
1851
    end
108,648✔
1852
    return takestring!(buf)
35,304✔
1853
end
1854

1855
function normalize_keys(keymap::Union{Dict{Char,Any},AnyDict})
1,078✔
1856
    ret = Dict{Any,Any}()
1,078✔
1857
    for (k,v) in keymap
1,096✔
1858
        normalized = normalize_key(k)
35,840✔
1859
        if haskey(ret,normalized)
35,840✔
1860
            error("""Multiple spellings of a key in a single keymap
3✔
1861
                     (\"$k\" conflicts with existing mapping)""")
1862
        end
1863
        ret[normalized] = v
35,837✔
1864
    end
70,596✔
1865
    return ret
1,075✔
1866
end
1867

1868
function add_nested_key!(keymap::Dict{Char, Any}, key::Union{String, Char}, value; override::Bool = false)
101,546✔
1869
    y = iterate(key)
71,674✔
1870
    while y !== nothing
108,164✔
1871
        c, i = y
216,322✔
1872
        y = iterate(key, i)
108,164✔
1873
        if !override && c in keys(keymap) && (y === nothing || !isa(keymap[c], Dict))
108,164✔
1874
            error("Conflicting definitions for keyseq " * escape_string(string(key)) *
6✔
1875
                  " within one keymap")
1876
        end
1877
        if y === nothing
108,158✔
1878
            keymap[c] = value
35,834✔
1879
            break
35,834✔
1880
        elseif !(c in keys(keymap) && isa(keymap[c], Dict))
72,324✔
1881
            keymap[c] = Dict{Char,Any}()
8,527✔
1882
        end
1883
        keymap = keymap[c]::Dict{Char, Any}
72,324✔
1884
    end
108,158✔
1885
end
1886

1887
# Redirect a key as if `seq` had been the keysequence instead in a lazy fashion.
1888
# This is different from the default eager redirect, which only looks at the current and lower
1889
# layers of the stack.
1890
struct KeyAlias
1891
    seq::String
1892
    KeyAlias(seq) = new(normalize_key(seq))
9✔
1893
end
1894

1895
function match_input(f::Function, s::Union{Nothing,MIState}, term, cs::Vector{Char}, keymap)
8,994✔
1896
    update_key_repeats(s, cs)
17,916✔
1897
    c = String(cs)
8,994✔
1898
    return function (s, p)  # s::Union{Nothing,MIState}; p can be (at least) a LineEditREPL, PrefixSearchState, Nothing
17,988✔
1899
        r = Base.invokelatest(f, s, p, c)
8,994✔
1900
        if isa(r, Symbol)
8,994✔
1901
            return r
441✔
1902
        else
1903
            return :ok
8,553✔
1904
        end
1905
    end
1906
end
1907

1908
match_input(k::Nothing, s, term, cs, keymap) = (s,p) -> return :ok
36✔
1909
match_input(k::KeyAlias, s::Union{Nothing,MIState}, term, cs, keymap::Dict{Char}) =
327✔
1910
    match_input(keymap, s, IOBuffer(k.seq), Char[], keymap)
1911

1912
function match_input(k::Dict{Char}, s::Union{Nothing,MIState}, term::Union{AbstractTerminal,IOBuffer}=terminal(s), cs::Vector{Char}=Char[], keymap::Dict{Char} = k)
18,540✔
1913
    # if we run out of characters to match before resolving an action,
1914
    # return an empty keymap function
1915
    eof(term) && return (s, p) -> :abort
63,054✔
1916
    c = read(term, Char)
9,861✔
1917
    # Ignore any `wildcard` as this is used as a
1918
    # placeholder for the wildcard (see normalize_key("*"))
1919
    c == wildcard && return (s, p) -> :ok
9,861✔
1920
    push!(cs, c)
9,861✔
1921
    key = haskey(k, c) ? c : wildcard
19,722✔
1922
    # if we don't match on the key, look for a default action then fallback on 'nothing' to ignore
1923
    return match_input(get(k, key, nothing), s, term, cs, keymap)
9,861✔
1924
end
1925

1926
update_key_repeats(s, keystroke) = nothing
72✔
1927
function update_key_repeats(s::MIState, keystroke::Vector{Char})
1928
    s.key_repeats  = s.previous_key == keystroke ? s.key_repeats + 1 : 0
17,474✔
1929
    s.previous_key = keystroke
8,922✔
1930
    return
8,922✔
1931
end
1932

1933

1934
## Conflict fixing
1935
# Consider a keymap of the form
1936
#
1937
# {
1938
#   "**" => f
1939
#   "ab" => g
1940
# }
1941
#
1942
# Naively this is transformed into a tree as
1943
#
1944
# {
1945
#   '*' => {
1946
#       '*' => f
1947
#   }
1948
#   'a' => {
1949
#       'b' => g
1950
#   }
1951
# }
1952
#
1953
# However, that's not what we want, because now "ac" is
1954
# is not defined. We need to fix this up and turn it into
1955
#
1956
# {
1957
#   '*' => {
1958
#       '*' => f
1959
#   }
1960
#   'a' => {
1961
#       '*' => f
1962
#       'b' => g
1963
#   }
1964
# }
1965
#
1966
# i.e. copy over the appropriate default subdict
1967
#
1968

1969
# deep merge where target has higher precedence
1970
function keymap_merge!(target::Dict{Char,Any}, source::Union{Dict{Char,Any},AnyDict})
6✔
1971
    for k in keys(source)
6✔
1972
        if !haskey(target, k)
6✔
UNCOV
1973
            target[k] = source[k]
×
1974
        elseif isa(target[k], Dict)
6✔
UNCOV
1975
            keymap_merge!(target[k], source[k])
×
1976
        else
1977
            # Ignore, target has higher precedence
1978
        end
1979
    end
6✔
1980
end
1981

1982
fixup_keymaps!(d, l, s, sk) = nothing
3,063✔
1983
function fixup_keymaps!(dict::Dict{Char,Any}, level, s, subkeymap)
4,116✔
1984
    if level > 0
4,116✔
1985
        for d in values(dict)
2,070✔
1986
            fixup_keymaps!(d, level-1, s, subkeymap)
6,147✔
1987
        end
12,294✔
1988
    else
1989
        if haskey(dict, s)
3,081✔
1990
            if isa(dict[s], Dict) && isa(subkeymap, Dict)
1,035✔
1991
                keymap_merge!(dict[s], subkeymap)
6✔
1992
            end
1993
        else
1994
            dict[s] = deepcopy(subkeymap)
2,046✔
1995
        end
1996
    end
1997
    nothing
4,116✔
1998
end
1999

2000
function add_specialisations(dict::Dict{Char,Any}, subdict::Dict{Char,Any}, level::Int)
5,546✔
2001
    default_branch = subdict[wildcard]
5,546✔
2002
    if isa(default_branch, Dict)
5,546✔
2003
        default_branch = default_branch::Dict{Char,Any}
1,029✔
2004
        # Go through all the keymaps in the default branch
2005
        # and copy them over to dict
2006
        for s in keys(default_branch)
2,058✔
2007
            s == wildcard && add_specialisations(dict, default_branch, level+1)
1,032✔
2008
            fixup_keymaps!(dict, level, s, default_branch[s])
1,032✔
2009
        end
1,032✔
2010
    end
2011
end
2012

2013
postprocess!(others) = nothing
26,852✔
2014
function postprocess!(dict::Dict{Char,Any})
7,774✔
2015
    # needs to be done first for every branch
2016
    if haskey(dict, wildcard)
15,548✔
2017
        add_specialisations(dict, dict, 1)
4,517✔
2018
    end
2019
    for (k,v) in dict
15,548✔
2020
        k == wildcard && continue
38,858✔
2021
        postprocess!(v)
34,341✔
2022
    end
38,858✔
2023
end
2024

2025
function getEntry(keymap::Dict{Char,Any},key::Union{String,Char})
6,922✔
2026
    v = keymap
6,922✔
2027
    for c in key
7,283✔
2028
        if !(haskey(v,c)::Bool)
6,922✔
UNCOV
2029
            return nothing
×
2030
        end
2031
        v = v[c]
6,922✔
2032
    end
6,922✔
2033
    return v
6,922✔
2034
end
2035

2036
# `target` is the total keymap being built up, already being a nested tree of Dicts.
2037
# source is the keymap specified by the user (with normalized keys)
2038
function keymap_merge(target::Dict{Char,Any}, source::Union{Dict{Char,Any},AnyDict})
1,075✔
2039
    ret = copy(target)
1,075✔
2040
    direct_keys = filter(p -> isa(p.second, Union{Function, KeyAlias, Nothing}), source)
36,909✔
2041
    # first direct entries
2042
    for key in keys(direct_keys)
2,150✔
2043
        add_nested_key!(ret, key, source[key]; override = true)
59,732✔
2044
    end
59,732✔
2045
    # then redirected entries
2046
    for key in setdiff(keys(source), keys(direct_keys))
2,150✔
2047
        key::Union{String, Char}
5,965✔
2048
        # We first resolve redirects in the source
2049
        value = source[key]
5,965✔
2050
        visited = Vector{Any}()
5,965✔
2051
        while isa(value, Union{Char,String})
11,924✔
2052
            value = normalize_key(value)
11,942✔
2053
            if value in visited
5,971✔
2054
                throw_eager_redirection_cycle(key)
3✔
2055
            end
2056
            push!(visited,value)
5,968✔
2057
            if !haskey(source,value)
5,968✔
2058
                break
9✔
2059
            end
2060
            value = source[value]
5,959✔
2061
        end
5,959✔
2062

2063
        if isa(value, Union{Char,String})
5,962✔
2064
            value = getEntry(ret, value)
18✔
2065
            if value === nothing
9✔
UNCOV
2066
                throw_could_not_find_redirected_value(key)
×
2067
            end
2068
        end
2069
        add_nested_key!(ret, key, value; override = true)
11,924✔
2070
    end
11,924✔
2071
    return ret
1,072✔
2072
end
2073

2074
throw_eager_redirection_cycle(key::Union{Char, String}) =
3✔
2075
    error("Eager redirection cycle detected for key ", repr(key))
UNCOV
2076
throw_could_not_find_redirected_value(key::Union{Char, String}) =
×
2077
    error("Could not find redirected value ", repr(key))
2078

2079
function keymap_unify(keymaps)
288✔
2080
    ret = Dict{Char,Any}()
288✔
2081
    for keymap in keymaps
288✔
2082
        ret = keymap_merge(ret, keymap)
1,075✔
2083
    end
1,072✔
2084
    postprocess!(ret)
285✔
2085
    return ret
285✔
2086
end
2087

2088
function validate_keymap(keymap)
285✔
2089
    for key in keys(keymap)
285✔
2090
        visited_keys = Any[key]
6,561✔
2091
        v = getEntry(keymap,key)
6,561✔
2092
        while isa(v,KeyAlias)
6,913✔
2093
            if v.seq in visited_keys
358✔
2094
                error("Alias cycle detected in keymap")
6✔
2095
            end
2096
            push!(visited_keys,v.seq)
352✔
2097
            v = getEntry(keymap,v.seq)
352✔
2098
        end
352✔
2099
    end
6,555✔
2100
end
2101

2102
function keymap(keymaps::Union{Vector{AnyDict},Vector{Dict{Char,Any}}})
24✔
2103
    # keymaps is a vector of prioritized keymaps, with highest priority first
2104
    ret = keymap_unify(map(normalize_keys, reverse(keymaps)))
300✔
2105
    validate_keymap(ret)
285✔
2106
    return ret
279✔
2107
end
2108

2109
const escape_defaults = merge!(
2110
    AnyDict(Char(i) => nothing for i=vcat(0:26, 28:31)), # Ignore control characters by default
2111
    AnyDict( # And ignore other escape sequences by default
2112
        "\e*" => nothing,
2113
        "\e[*" => nothing,
2114
        "\eO*" => nothing,
2115
        # Intercept DA1 responses
UNCOV
2116
        "\e[?" => (s::MIState, o...) -> receive_da1!(s.terminal_properties, terminal(s)),
×
2117
        # Also ignore extended escape sequences
2118
        # TODO: Support ranges of characters
2119
        "\e[1**" => nothing,
2120
        "\e[2**" => nothing,
2121
        "\e[3**" => nothing,
2122
        "\e[4**" => nothing,
2123
        "\e[5**" => nothing,
2124
        "\e[6**" => nothing,
2125
        # less commonly used VT220 editing keys
2126
        "\e[2~" => nothing, # insert
2127
        "\e[3~" => nothing, # delete
2128
        "\e[5~" => nothing, # page up
2129
        "\e[6~" => nothing, # page down
2130
        # These are different spellings of arrow keys, home keys, etc.
2131
        # and should always do the same as the canonical key sequence
2132
        "\e[1~" => KeyAlias("\e[H"), # home
2133
        "\e[4~" => KeyAlias("\e[F"), # end
2134
        "\e[7~" => KeyAlias("\e[H"), # home
2135
        "\e[8~" => KeyAlias("\e[F"), # end
2136
        "\eOA"  => KeyAlias("\e[A"),
2137
        "\eOB"  => KeyAlias("\e[B"),
2138
        "\eOC"  => KeyAlias("\e[C"),
2139
        "\eOD"  => KeyAlias("\e[D"),
2140
        "\eOH"  => KeyAlias("\e[H"),
2141
        "\eOF"  => KeyAlias("\e[F"),
2142
    ),
2143
    # set mode commands
2144
    AnyDict("\e[$(c)h" => nothing for c in 1:20),
2145
    # reset mode commands
2146
    AnyDict("\e[$(c)l" => nothing for c in 1:20)
2147
    )
2148

2149

2150
# Helper function to check and remove paired brackets/quotes
2151
# Returns true if paired delimiters were removed, false otherwise
2152
function try_remove_paired_delimiter(buf::IOBuffer)
21✔
2153
    left_brackets = ('(', '{', '[', '"', '\'', '`')
21✔
2154
    right_brackets = (')', '}', ']', '"', '\'', '`')
21✔
2155

2156
    if !eof(buf) && position(buf) > 0
21✔
2157
        # Peek at char to the left
2158
        p = position(buf)
9✔
2159
        left_char = char_move_left(buf)
9✔
2160
        seek(buf, p)
18✔
2161

2162
        i = findfirst(isequal(left_char), left_brackets)
12✔
2163
        if i !== nothing && peek(buf, Char) == right_brackets[i]
9✔
2164
            # Remove both the left and right bracket/quote
2165
            edit_delete(buf)
12✔
2166
            edit_backspace(buf)
6✔
2167
            return true
6✔
2168
        end
2169
    end
2170
    return false
15✔
2171
end
2172

2173
# Keymap for automatic bracket/quote insertion and completion
2174
const bracket_insert_keymap = AnyDict()
2175
let
2176
    # Determine when we should not close a bracket/quote
2177
    function should_skip_closing_bracket(left_peek, v)
33✔
2178
        # Don't close if we already have an open quote immediately before (triple quote case)
2179
        # For quotes, also check for transpose expressions: issue JuliaLang/OhMyREPL.jl#200
2180
        left_peek == v && return true
33✔
2181
        if v == '\''
30✔
2182
            tr_expr = isletter(left_peek) || isnumeric(left_peek) || left_peek == '_' || left_peek == ']'
21✔
2183
            return tr_expr
12✔
2184
        end
2185
        return false
18✔
2186
    end
2187

2188
    function peek_char_left(b::IOBuffer)
2189
        p = position(b)
54✔
2190
        c = char_move_left(b)
54✔
2191
        seek(b, p)
108✔
2192
        return c
54✔
2193
    end
2194

2195
    # Check if we should auto-close a quote (insert paired quotes)
2196
    # auto-close when "transparent" chars on both sides
2197
    # Transparent chars: whitespace, opening brackets ([{, closing brackets )]}, or nothing
2198
    function should_auto_close_quote(buf::IOBuffer, quote_char::Char)
45✔
2199
        # Check left side: BOF, whitespace, or opening bracket
2200
        left_ok = if position(buf) == 0
45✔
2201
            true
24✔
2202
        else
2203
            left_char = peek_char_left(buf)
42✔
2204
            isspace(left_char) || left_char in ('(', '[', '{')
66✔
2205
        end
2206

2207
        # Check right side: EOF, whitespace, or closing bracket
2208
        right_ok = if eof(buf)
45✔
2209
            true
33✔
2210
        else
2211
            right_char = peek(buf, Char)
12✔
2212
            isspace(right_char) || right_char in (')', ']', '}')
57✔
2213
        end
2214

2215
        return left_ok && right_ok
45✔
2216
    end
2217

2218
    # Left/right bracket pairs
2219
    bracket_pairs = (('(', ')'), ('{', '}'), ('[', ']'))
2220
    # Characters that are "transparent" for bracket auto-closing
2221
    right_brackets_ws = (')', '}', ']', ' ', '\t', '\n', '"', '\'', '`')
2222

2223
    for (left, right) in bracket_pairs
2224
        # Left bracket: insert both and move cursor between them
2225
        bracket_insert_keymap[left] = (s::MIState, o...) -> begin
42✔
2226
            local buf = buffer(s)
42✔
2227
            edit_insert(buf, left)
60✔
2228
            if eof(buf) || peek(buf, Char) in right_brackets_ws
60✔
2229
                edit_insert(buf, right)
54✔
2230
                edit_move_left(buf)
39✔
2231
            end
2232
            refresh_line(s)
42✔
2233
        end
2234

2235
        # Right bracket: skip over if next char matches, otherwise insert
2236
        bracket_insert_keymap[right] = (s::MIState, o...) -> begin
30✔
2237
            local buf = buffer(s)
30✔
2238
            if !eof(buf) && peek(buf, Char) == right
30✔
2239
                edit_move_right(buf)
18✔
2240
            else
2241
                edit_insert(buf, right)
12✔
2242
            end
2243
            refresh_line(s)
30✔
2244
        end
2245
    end
2246

2247
    # Quote characters (need special handling for transpose detection)
2248
    for quote_char in ('"', '\'', '`')
2249
        bracket_insert_keymap[quote_char] = (s::MIState, o...) -> begin
66✔
2250
            local buf = buffer(s)
66✔
2251
            if !eof(buf) && peek(buf, Char) == quote_char
66✔
2252
                # Skip over closing quote
2253
                edit_move_right(buf)
9✔
2254
            elseif position(buf) > 0 && should_skip_closing_bracket(peek_char_left(buf), quote_char)
90✔
2255
                # Don't auto-close (e.g., for transpose or triple quotes)
2256
                edit_insert(buf, quote_char)
12✔
2257
            elseif should_auto_close_quote(buf, quote_char)
45✔
2258
                edit_insert(buf, quote_char)
39✔
2259
                edit_insert(buf, quote_char)
39✔
2260
                edit_move_left(buf)
33✔
2261
            else
2262
                # Just insert single quote
2263
                edit_insert(buf, quote_char)
12✔
2264
            end
2265
            refresh_line(s)
66✔
2266
        end
2267
    end
2268

2269
    # Backspace - also remove matching closing bracket/quote
2270
    bracket_insert_keymap['\b'] = (s::MIState, o...) -> begin
21✔
2271
        if is_region_active(s)
21✔
UNCOV
2272
            return edit_kill_region(s)
×
2273
        elseif isempty(s) || position(buffer(s)) == 0
42✔
2274
            # Handle transitioning to main mode
UNCOV
2275
            repl = Base.active_repl
×
UNCOV
2276
            mirepl = isdefined(repl, :mi) ? repl.mi : repl
×
UNCOV
2277
            main_mode = mirepl.interface.modes[1]
×
UNCOV
2278
            local buf = copy(buffer(s))
×
UNCOV
2279
            transition(s, main_mode) do
×
2280
                state(s, main_mode).input_buffer = buf
2281
            end
UNCOV
2282
            return
×
2283
        end
2284

2285
        if try_remove_paired_delimiter(buffer(s))
21✔
2286
            return refresh_line(s)
6✔
2287
        end
2288
        return edit_backspace(s)
15✔
2289
    end
2290
end
2291

2292
# a meta-prompt that presents itself as parent_prompt, but which has an independent keymap
2293
# for prefix searching
2294
mutable struct PrefixHistoryPrompt <: TextInterface
2295
    hp::HistoryProvider
2296
    parent_prompt::Prompt
2297
    complete::CompletionProvider
2298
    keymap_dict::Dict{Char,Any}
2299
    PrefixHistoryPrompt(hp, parent_prompt) =
85✔
2300
        new(hp, parent_prompt, EmptyCompletionProvider())
2301
end
2302

2303
mutable struct PrefixSearchState <: ModeState
2304
    terminal::AbstractTerminal
2305
    histprompt::PrefixHistoryPrompt
2306
    prefix::String
2307
    response_buffer::IOBuffer
2308
    ias::InputAreaState
2309
    indent::Int
2310
    # The modal interface state, if present
2311
    mi::MIState
2312
    #The prompt whose input will be replaced by the matched history
2313
    parent::Prompt
2314
    PrefixSearchState(terminal, histprompt, prefix, response_buffer) =
91✔
2315
        new(terminal, histprompt, prefix, response_buffer, InputAreaState(0,0), 0)
2316
end
2317

2318
# interface for ModeState
2319
function Base.getproperty(s::ModeState, name::Symbol)
6✔
2320
    if name === :terminal
326,059✔
2321
        return getfield(s, :terminal)::AbstractTerminal
22,651✔
2322
    elseif name === :prompt
303,408✔
UNCOV
2323
        return getfield(s, :prompt)::Prompt
×
2324
    elseif name === :histprompt
303,408✔
2325
        return getfield(s, :histprompt)::PrefixHistoryPrompt
992✔
2326
    elseif name === :parent
302,416✔
2327
        return getfield(s, :parent)::Prompt
477✔
2328
    elseif name === :response_buffer
301,939✔
2329
        return getfield(s, :response_buffer)::IOBuffer
1,370✔
2330
    elseif name === :ias
300,569✔
2331
        return getfield(s, :ias)::InputAreaState
13,740✔
2332
    elseif name === :indent
286,829✔
2333
        return getfield(s, :indent)::Int
6,954✔
2334
    # # unique fields, but no harm in declaring them
2335
    # elseif name === :input_buffer
2336
    #     return getfield(s, :input_buffer)::IOBuffer
2337
    # elseif name === :region_active
2338
    #     return getfield(s, :region_active)::Symbol
2339
    # elseif name === :undo_buffers
2340
    #     return getfield(s, :undo_buffers)::Vector{IOBuffer}
2341
    # elseif name === :undo_idx
2342
    end
2343
    return getfield(s, name)
279,875✔
2344
end
2345

2346
init_state(terminal, p::PrefixHistoryPrompt) = PrefixSearchState(terminal, p, "", IOBuffer())
91✔
2347

UNCOV
2348
function show(io::IO, s::PrefixSearchState)
×
UNCOV
2349
    print(io, "PrefixSearchState ", isdefined(s,:parent) ?
×
2350
     string("(", s.parent, " active)") : "(no parent)", " for ",
2351
     isdefined(s,:mi) ? s.mi : "no MI")
2352
end
2353

2354
function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal,
4,964✔
2355
                            s::Union{PromptState,PrefixSearchState}; beeping::Bool=false)
2356
    beeping || cancel_beep(s)
4,964✔
2357
    ias = refresh_multi_line(termbuf, terminal, buffer(s), s.ias, s;
4,964✔
2358
                             indent = s.indent,
2359
                             region_active = is_region_active(s))
2360
    s.ias = ias
4,964✔
2361
    return ias
4,964✔
2362
end
2363

2364
input_string(s::PrefixSearchState) = takestring!(copy(s.response_buffer))
105✔
2365

2366
write_prompt(terminal, s::PrefixSearchState, color::Bool) = write_prompt(terminal, s.histprompt.parent_prompt, color)
201✔
UNCOV
2367
prompt_string(s::PrefixSearchState) = prompt_string(s.histprompt.parent_prompt.prompt)
×
2368

2369
terminal(s::PrefixSearchState) = s.terminal
336✔
2370

2371
function reset_state(s::PrefixSearchState)
389✔
2372
    if s.response_buffer.size != 0
389✔
2373
        s.response_buffer.size = 0
24✔
2374
        s.response_buffer.ptr = 1
24✔
2375
    end
2376
    reset_state(s.histprompt.hp)
389✔
2377
    nothing
389✔
2378
end
2379

2380
function transition(f::Function, s::PrefixSearchState, mode::Prompt)
12✔
2381
    if isdefined(s, :mi)
96✔
2382
        transition(s.mi, mode)
72✔
2383
    end
2384
    s.parent = mode
96✔
2385
    s.histprompt.parent_prompt = mode
96✔
2386
    if isdefined(s, :mi)
96✔
2387
        transition(f, s.mi, s.histprompt)
72✔
2388
    else
2389
        f()
24✔
2390
    end
2391
    nothing
96✔
2392
end
2393

2394
replace_line(s::PrefixSearchState, l::IOBuffer) = (s.response_buffer = l; nothing)
12✔
2395
function replace_line(s::PrefixSearchState, l::Union{String,SubString{String}})
2396
    s.response_buffer.ptr = 1
84✔
2397
    s.response_buffer.size = 0
84✔
2398
    write(s.response_buffer, l)
84✔
2399
    nothing
84✔
2400
end
2401

2402
state(s::MIState, p::TextInterface=mode(s)) = s.mode_state[p]
141,812✔
UNCOV
2403
state(s::PromptState, p::Prompt=mode(s)) = (@assert s.p == p; s)
×
2404

2405
mode(s::MIState) = s.current_mode   # ::TextInterface, and might be a Prompt
91,209✔
2406
mode(s::PromptState) = s.p          # ::Prompt
1,068✔
2407
mode(s::PrefixSearchState) = s.histprompt.parent_prompt   # ::Prompt
138✔
2408

UNCOV
2409
setmodifiers!(s::MIState, m::Modifiers) = setmodifiers!(mode(s), m)
×
UNCOV
2410
setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m)
×
UNCOV
2411
setmodifiers!(c) = nothing
×
2412

UNCOV
2413
accept_result_newmode(hp::HistoryProvider) = nothing
×
2414
function accept_result(s::MIState, p::TextInterface)
33✔
2415
    parent = something(accept_result_newmode(p.hp), state(s, p).parent)
33✔
2416
    transition(s, parent) do
33✔
2417
        replace_line(state(s, parent), state(s, p).response_buffer)
33✔
2418
        nothing
33✔
2419
    end
2420
    nothing
33✔
2421
end
2422

2423
function copybuf!(dst::IOBuffer, src::IOBuffer)
2424
    n = src.size
33✔
2425
    ensureroom(dst, n)
66✔
2426
    copyto!(dst.data, 1, src.data, 1, n)
39✔
2427
    dst.size = src.size
33✔
2428
    dst.ptr = src.ptr
33✔
2429
    nothing
33✔
2430
end
2431

2432
function enter_prefix_search(s::MIState, p::PrefixHistoryPrompt, backward::Bool)
33✔
2433
    buf = copy(buffer(s))
33✔
2434
    parent = mode(s)
33✔
2435

2436
    transition(s, p) do
33✔
2437
        local pss = state(s, p)
33✔
2438
        pss.parent = parent
33✔
2439
        pss.histprompt.parent_prompt = parent
33✔
2440
        pss.prefix = String(buf.data[1:position(buf)])
33✔
2441
        copybuf!(pss.response_buffer, buf)
39✔
2442
        pss.indent = state(s, parent).indent
33✔
2443
        pss.mi = s
33✔
2444
    end
2445
    pss = state(s, p)
33✔
2446
    if backward
33✔
2447
        history_prev_prefix(pss, pss.histprompt.hp, pss.prefix)
30✔
2448
    else
2449
        history_next_prefix(pss, pss.histprompt.hp, pss.prefix)
3✔
2450
    end
2451
    nothing
33✔
2452
end
2453

2454
keymap(state, p::PrefixHistoryPrompt) = p.keymap_dict
30✔
2455
keymap_data(state, ::PrefixHistoryPrompt) = state
30✔
2456

2457
Base.isempty(s::PromptState) = s.input_buffer.size == 0
215✔
2458

2459
on_enter(s::MIState) = state(s).p.on_enter(s)
336✔
2460

2461
move_input_start(s::BufferLike) = (seek(buffer(s), 0); nothing)
126✔
2462
move_input_end(buf::IOBuffer) = (seekend(buf); nothing)
582✔
2463
move_input_end(s::Union{MIState,ModeState}) = (move_input_end(buffer(s)); nothing)
483✔
2464

2465
function move_line_start(s::MIState)
39✔
2466
    set_action!(s, :move_line_start)
39✔
2467
    buf = buffer(s)
39✔
2468
    curpos = position(buf)
39✔
2469
    curpos == 0 && return
39✔
2470
    if s.key_repeats > 0
36✔
2471
        move_input_start(s)
6✔
2472
    else
2473
        seek(buf, something(findprev(isequal(UInt8('\n')), buf.data, curpos), 0))
33✔
2474
    end
2475
    nothing
36✔
2476
end
2477

2478
function move_line_end(s::MIState)
21✔
2479
    set_action!(s, :move_line_end)
81✔
2480
    s.key_repeats > 0 ?
159✔
2481
        move_input_end(s) :
2482
        move_line_end(buffer(s))
2483
    nothing
81✔
2484
end
2485

2486
function move_line_end(buf::IOBuffer)
120✔
2487
    eof(buf) && return
120✔
2488
    @views pos = findnext(isequal(UInt8('\n')), buf.data[1:buf.size], position(buf)+1)
240✔
2489
    if pos === nothing
120✔
2490
        move_input_end(buf)
99✔
2491
        return
99✔
2492
    end
2493
    seek(buf, pos - 1)
42✔
2494
    nothing
21✔
2495
end
2496

2497
function edit_insert_last_word(s::MIState)
6✔
2498
    hist = mode(s).hist.history
6✔
2499
    isempty(hist) && return 0
6✔
2500
    isempty(hist.records) && return 0
6✔
2501
    edit_insert(s, get_last_word(IOBuffer(hist[end].content)))
6✔
2502
end
2503

2504
function get_last_word(buf::IOBuffer)
42✔
2505
    move_line_end(buf)
42✔
2506
    char_move_word_left(buf)
42✔
2507
    posbeg = position(buf)
42✔
2508
    char_move_word_right(buf)
42✔
2509
    posend = position(buf)
42✔
2510
    buf = take!(buf)
42✔
2511
    word = String(buf[posbeg+1:posend])
84✔
2512
    rest = String(buf[posend+1:end])
69✔
2513
    lp, rp, lb, rb = count.(.==(('(', ')', '[', ']')), rest)
42✔
2514
    special = any(in.(('\'', '"', '`'), rest))
126✔
2515
    !special && lp == rp && lb == rb ?
42✔
2516
        word *= rest :
2517
        word
2518
end
2519

2520
function commit_line(s::MIState)
360✔
2521
    cancel_beep(s)
360✔
2522
    move_input_end(s)
360✔
2523
    refresh_line(s)
360✔
2524
    println(terminal(s))
360✔
2525
    add_history(s)
360✔
2526
    ias = InputAreaState(0, 0)
360✔
2527
    state(s, mode(s)).ias = ias
360✔
2528
    nothing
360✔
2529
end
2530

2531
function bracketed_paste(s::MIState; tabwidth::Int=options(s).tabwidth)
48✔
2532
    options(s).auto_indent_bracketed_paste = true
24✔
2533
    ps = state(s, mode(s))::PromptState
24✔
2534
    input = readuntil(ps.terminal, "\e[201~")
24✔
2535
    input = replace(input, '\r' => '\n')
24✔
2536
    if position(buffer(s)) == 0
24✔
2537
        indent = Base.indentation(input; tabwidth=tabwidth)[1]
21✔
2538
        input = Base.unindent(input, indent; tabwidth=tabwidth)
21✔
2539
    end
2540
    return replace(input, '\t' => " "^tabwidth)
24✔
2541
end
2542

2543
function tab_should_complete(s::MIState)
2544
    # Yes, we are ignoring the possibility
2545
    # the we could be in the middle of a multi-byte
2546
    # sequence, here but that's ok, since any
2547
    # whitespace we're interested in is only one byte
2548
    buf = buffer(s)
54✔
2549
    pos = position(buf)
54✔
2550
    pos == 0 && return true
54✔
2551
    c = buf.data[pos]
51✔
2552
    return c != _newline && c != UInt8('\t') &&
51✔
2553
        # hack to allow path completion in cmds
2554
        # after a space, e.g., `cd <tab>`, while still
2555
        # allowing multiple indent levels
2556
        (c != _space || pos <= 3 || buf.data[pos-1] != _space)
2557
end
2558

2559
# jump_spaces: if cursor is on a ' ', move it to the first non-' ' char on the right
2560
# if `delete_trailing`, ignore trailing ' ' by deleting them
2561
function edit_tab(s::MIState, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces)
75✔
2562
    tab_should_complete(s) && return complete_line(s)
162✔
2563
    set_action!(s, :edit_insert_tab)
36✔
2564
    push_undo(s)
36✔
2565
    edit_insert_tab(buffer(s), jump_spaces, delete_trailing) || pop_undo(s)
45✔
2566
    return refresh_line(s)
36✔
2567
end
2568

UNCOV
2569
function shift_tab_completion(s::MIState)
×
2570
    setmodifiers!(s, Modifiers(true))
×
UNCOV
2571
    return complete_line(s)
×
2572
end
2573

2574
# return true iff the content of the buffer is modified
2575
# return false when only the position changed
2576
function edit_insert_tab(buf::IOBuffer, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces)
36✔
2577
    i = position(buf)
36✔
2578
    if jump_spaces && i < buf.size && buf.data[i+1] == _space
36✔
2579
        spaces = something(findnext(_notspace, buf.data[i+1:buf.size], 1), 0)
24✔
2580
        if delete_trailing && (spaces == 0 || buf.data[i+spaces] == _newline)
21✔
2581
            edit_splice!(buf, i => (spaces == 0 ? buf.size : i+spaces-1))
9✔
2582
        else
2583
            jump = spaces == 0 ? buf.size : i+spaces-1
15✔
2584
            seek(buf, jump)
18✔
2585
            return false
9✔
2586
        end
2587
    end
2588
    # align to multiples of 4:
2589
    align = 4 - textwidth(String(buf.data[1+beginofline(buf, i):i])) % 4
51✔
2590
    edit_insert(buf, ' '^align)
36✔
2591
    return true
27✔
2592
end
2593

2594
function edit_abort(s::MIState, confirm::Bool=options(s).confirm_exit; key="^D")
207✔
2595
    set_action!(s, :edit_abort)
69✔
2596
    if !confirm || s.last_action === :edit_abort
69✔
2597
        println(terminal(s))
69✔
2598
        return :abort
69✔
2599
    else
2600
        println("Type $key again to exit.\n")
×
2601
        return refresh_line(s)
×
2602
    end
2603
end
2604

2605
const default_keymap =
2606
AnyDict(
2607
    # Tab
2608
    '\t' => (s::MIState,o...)->edit_tab(s, true),
15✔
2609
    # Shift-tab
2610
    "\e[Z" => (s::MIState,o...)->shift_tab_completion(s),
×
2611
    # Enter
2612
    '\r' => (s::MIState,o...)->begin
336✔
2613
        if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1)
342✔
2614
            commit_line(s)
330✔
2615
            return :done
330✔
2616
        else
2617
            edit_insert_newline(s)
6✔
2618
        end
2619
    end,
2620
    '\n' => KeyAlias('\r'),
2621
    # Backspace/^H
2622
    '\b' => (s::MIState,o...) -> is_region_active(s) ? edit_kill_region(s) : edit_backspace(s),
63✔
2623
    127 => KeyAlias('\b'),
2624
    # Meta Backspace
2625
    "\e\b" => (s::MIState,o...)->edit_delete_prev_word(s),
×
2626
    "\e\x7f" => "\e\b",
2627
    # ^D
2628
    "^D" => (s::MIState,o...)->begin
69✔
2629
        if buffer(s).size > 0
69✔
UNCOV
2630
            edit_delete(s)
×
2631
        else
2632
            edit_abort(s)
69✔
2633
        end
2634
    end,
2635
    # Ctrl-Space
2636
    "\0" => (s::MIState,o...)->setmark(s),
3✔
2637
    "^G" => (s::MIState,o...)->(deactivate_region(s); refresh_line(s)),
×
2638
    "^X^X" => (s::MIState,o...)->edit_exchange_point_and_mark(s),
6✔
UNCOV
2639
    "^B" => (s::MIState,o...)->edit_move_left(s),
×
2640
    "^F" => (s::MIState,o...)->edit_move_right(s),
×
2641
    "^P" => (s::MIState,o...)->edit_move_up(s),
×
2642
    "^N" => (s::MIState,o...)->edit_move_down(s),
×
2643
    # Meta-Up
UNCOV
2644
    "\e[1;3A" => (s::MIState,o...) -> edit_transpose_lines_up!(s),
×
2645
    # Meta-Down
UNCOV
2646
    "\e[1;3B" => (s::MIState,o...) -> edit_transpose_lines_down!(s),
×
UNCOV
2647
    "\e[1;2D" => (s::MIState,o...)->edit_shift_move(s, edit_move_left),
×
UNCOV
2648
    "\e[1;2C" => (s::MIState,o...)->edit_shift_move(s, edit_move_right),
×
2649
    "\e[1;2A" => (s::MIState,o...)->edit_shift_move(s, edit_move_up),
×
UNCOV
2650
    "\e[1;2B" => (s::MIState,o...)->edit_shift_move(s, edit_move_down),
×
2651
    # Meta B
UNCOV
2652
    "\eb" => (s::MIState,o...)->edit_move_word_left(s),
×
2653
    # Meta F
UNCOV
2654
    "\ef" => (s::MIState,o...)->edit_move_word_right(s),
×
2655
    # Ctrl-Left Arrow
2656
    "\e[1;5D" => "\eb",
2657
    # Ctrl-Left Arrow on rxvt
2658
    "\eOd" => "\eb",
2659
    # Ctrl-Right Arrow
2660
    "\e[1;5C" => "\ef",
2661
    # Ctrl-Right Arrow on rxvt
2662
    "\eOc" => "\ef",
2663
    # Meta Enter
2664
    "\e\r" => (s::MIState,o...)->edit_insert_newline(s),
12✔
UNCOV
2665
    "\e." =>  (s::MIState,o...)->edit_insert_last_word(s),
×
2666
    "\e\n" => "\e\r",
UNCOV
2667
    "^_" => (s::MIState,o...)->edit_undo!(s),
×
UNCOV
2668
    "\e_" => (s::MIState,o...)->edit_redo!(s),
×
2669
    # Show hints at what tab complete would do by default
2670
    "*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_show_hint(s)),
7,837✔
2671
    "^U" => (s::MIState,o...)->edit_kill_line_backwards(s),
21✔
2672
    "^K" => (s::MIState,o...)->edit_kill_line_forwards(s),
×
2673
    "^Y" => (s::MIState,o...)->edit_yank(s),
6✔
2674
    "\ey" => (s::MIState,o...)->edit_yank_pop(s),
×
2675
    "\ew" => (s::MIState,o...)->edit_copy_region(s),
×
2676
    "\eW" => (s::MIState,o...)->edit_kill_region(s),
×
UNCOV
2677
    "^A" => (s::MIState,o...)->(move_line_start(s); refresh_line(s)),
×
2678
    "^E" => (s::MIState,o...)->(move_line_end(s); refresh_line(s)),
×
2679
    # Try to catch all Home/End keys
2680
    "\e[H"  => (s::MIState,o...)->(move_input_start(s); refresh_line(s)),
×
2681
    "\e[F"  => (s::MIState,o...)->(move_input_end(s); refresh_line(s)),
×
2682
    "^L" => (s::MIState,o...)->(Terminals.clear(terminal(s)); refresh_line(s)),
×
2683
    "^W" => (s::MIState,o...)->edit_werase(s),
×
2684
    # Meta D
UNCOV
2685
    "\ed" => (s::MIState,o...)->edit_delete_next_word(s),
×
2686
    "^C" => (s::MIState,o...)->begin
27✔
2687
        try # raise the debugger if present
27✔
2688
            ccall(:jl_raise_debugger, Int, ())
27✔
2689
        catch
×
2690
        end
2691
        cancel_beep(s)
27✔
2692
        move_input_end(s)
27✔
2693
        refresh_line(s)
27✔
2694
        print(terminal(s), "^C\n\n")
27✔
2695
        transition(s, :reset)
27✔
2696
        refresh_line(s)
27✔
2697
    end,
2698
    "^Z" => (s::MIState,o...)->(return :suspend),
×
2699
    # Right Arrow
2700
    "\e[C" => (s::MIState,o...)->edit_move_right(s),
×
2701
    # Left Arrow
2702
    "\e[D" => (s::MIState,o...)->edit_move_left(s),
54✔
2703
    # Up Arrow
2704
    "\e[A" => (s::MIState,o...)->edit_move_up(s),
×
2705
    # Down Arrow
UNCOV
2706
    "\e[B" => (s::MIState,o...)->edit_move_down(s),
×
2707
    # Meta-Right Arrow
2708
    "\e[1;3C" => (s::MIState,o...) -> edit_indent_right(s, 1),
3✔
2709
    # Meta-Left Arrow
2710
    "\e[1;3D" => (s::MIState,o...) -> edit_indent_left(s, 1),
×
2711
    # Delete
2712
    "\e[3~" => (s::MIState,o...)->edit_delete(s),
×
2713
    # Bracketed Paste Mode
2714
    "\e[200~" => (s::MIState,o...)->begin
×
UNCOV
2715
        input = bracketed_paste(s)
×
2716
        edit_insert(s, input)
×
2717
    end,
UNCOV
2718
    "^T" => (s::MIState,o...)->edit_transpose_chars(s),
×
2719
    "\et" => (s::MIState,o...)->edit_transpose_words(s),
×
UNCOV
2720
    "\eu" => (s::MIState,o...)->edit_upper_case(s),
×
2721
    "\el" => (s::MIState,o...)->edit_lower_case(s),
×
2722
    "\ec" => (s::MIState,o...)->edit_title_case(s),
×
2723
    "\ee" => (s::MIState,o...) -> edit_input(s),
×
UNCOV
2724
    "\em" => (s::MIState, o...) -> activate_module(s)
×
2725
)
2726

2727
const history_keymap = AnyDict(
UNCOV
2728
    "^R" => (s::MIState,o...)->(history_search(s)),
×
2729
    "^S" => (s::MIState,o...)->(history_search(s)),
×
2730
    # C/M-n/p
2731
    "^P" => (s::MIState,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)),
×
2732
    "^N" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)),
×
2733
    "\ep" => (s::MIState,o...)->(history_prev(s, mode(s).hist)),
×
2734
    "\en" => (s::MIState,o...)->(history_next(s, mode(s).hist)),
×
2735
    # Up Arrow
2736
    "\e[A" => (s::MIState,o...)->(edit_move_up(s) || history_prev(s, mode(s).hist)),
×
2737
    # Down Arrow
2738
    "\e[B" => (s::MIState,o...)->(edit_move_down(s) || history_next(s, mode(s).hist)),
×
2739
    # Page Up
2740
    "\e[5~" => (s::MIState,o...)->(history_prev(s, mode(s).hist)),
×
2741
    # Page Down
UNCOV
2742
    "\e[6~" => (s::MIState,o...)->(history_next(s, mode(s).hist)),
×
UNCOV
2743
    "\e<" => (s::MIState,o...)->(history_first(s, mode(s).hist)),
×
UNCOV
2744
    "\e>" => (s::MIState,o...)->(history_last(s, mode(s).hist)),
×
2745
)
2746

2747
function history_search(mistate::MIState)
×
UNCOV
2748
    cancel_beep(mistate)
×
UNCOV
2749
    termbuf = TerminalBuffer(IOBuffer())
×
UNCOV
2750
    term = terminal(mistate)
×
2751
    mimode = mode(mistate)
×
UNCOV
2752
    mimode.hist.last_mode = mimode
×
UNCOV
2753
    mimode.hist.last_buffer = copy(buffer(mistate))
×
UNCOV
2754
    mistate.mode_state[mimode] =
×
2755
        deactivate(mimode, state(mistate), termbuf, term)
UNCOV
2756
    prefix = if mimode.prompt_prefix isa Function
×
UNCOV
2757
        mimode.prompt_prefix()
×
2758
    else
UNCOV
2759
        mimode.prompt_prefix
×
2760
    end
2761
    # Issue a DA1 query if we haven't received one yet, so that the
2762
    # terminal's OSC 52 clipboard capability can be detected.
NEW
2763
    if mistate.terminal_properties.da1 === nothing
×
NEW
2764
        write(term, "\e[c")
×
2765
    end
NEW
2766
    result = histsearch(mimode.hist.history, term, prefix, mistate.terminal_properties)
×
UNCOV
2767
    mimode = if isnothing(result.mode)
×
UNCOV
2768
        mistate.current_mode
×
2769
    else
UNCOV
2770
        get(mistate.interface.modes[1].hist.mode_mapping,
×
2771
            result.mode,
2772
            mistate.current_mode)
2773
    end
UNCOV
2774
    pstate = mistate.mode_state[mimode]
×
UNCOV
2775
    raw!(term, true)
×
UNCOV
2776
    mistate.current_mode = mimode
×
UNCOV
2777
    activate(mimode, state(mistate, mimode), termbuf, term)
×
UNCOV
2778
    commit_changes(term, termbuf)
×
UNCOV
2779
if !isempty(result.text)
×
UNCOV
2780
    pstate.input_buffer.ptr = 1
×
UNCOV
2781
    pstate.input_buffer.size = 0
×
UNCOV
2782
    write(pstate.input_buffer, result.text)
×
UNCOV
2783
    seekend(pstate.input_buffer)
×
2784
end
UNCOV
2785
    refresh_multi_line(mistate)
×
UNCOV
2786
    nothing
×
2787
end
2788

2789
const prefix_history_keymap = merge!(
2790
    AnyDict(
UNCOV
2791
        "^P" => (s::MIState,data::ModeState,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix),
×
UNCOV
2792
        "^N" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix),
×
2793
        # Up Arrow
2794
        "\e[A" => (s::MIState,data::ModeState,c)->history_prev_prefix(data, data.histprompt.hp, data.prefix),
3✔
2795
        # Down Arrow
UNCOV
2796
        "\e[B" => (s::MIState,data::ModeState,c)->history_next_prefix(data, data.histprompt.hp, data.prefix),
×
2797
        # by default, pass through to the parent mode
2798
        "*"    => (s::MIState,data::ModeState,c::StringLike)->begin
27✔
2799
            accept_result(s, data.histprompt);
27✔
2800
            ps = state(s, mode(s))
27✔
2801
            map = keymap(ps, mode(s))
27✔
2802
            match_input(map, s, IOBuffer(c))(s, keymap_data(ps, mode(s)))
27✔
2803
        end,
2804
        # match escape sequences for pass through
2805
        "^x*" => "*",
2806
        "\em*" => "*",
2807
        "\e*" => "*",
2808
        "\e[*" => "*",
2809
        "\eO*"  => "*",
2810
        "\e[?" => "*",
2811
        "\e[1;5*" => "*", # Ctrl-Arrow
2812
        "\e[1;2*" => "*", # Shift-Arrow
2813
        "\e[1;3*" => "*", # Meta-Arrow
2814
        "\e[200~" => "*"
2815
    ),
2816
    # VT220 editing commands
2817
    AnyDict("\e[$(n)~" => "*" for n in 1:8),
2818
    # set mode commands
2819
    AnyDict("\e[$(c)h" => "*" for c in 1:20),
2820
    # reset mode commands
2821
    AnyDict("\e[$(c)l" => "*" for c in 1:20)
2822
)
2823

2824
function setup_prefix_keymap(hp::HistoryProvider, parent_prompt::Prompt)
85✔
2825
    p = PrefixHistoryPrompt(hp, parent_prompt)
85✔
2826
    p.keymap_dict = keymap([prefix_history_keymap])
85✔
2827
    pkeymap = AnyDict(
85✔
2828
        "^P" => (s::MIState,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)),
2829
        "^N" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)),
2830
        # Up Arrow
2831
        "\e[A" => (s::MIState,o...)->(edit_move_up(s) || enter_prefix_search(s, p, true)),
24✔
2832
        # Down Arrow
2833
        "\e[B" => (s::MIState,o...)->(edit_move_down(s) || enter_prefix_search(s, p, false)),
3✔
2834
    )
2835
    return (p, pkeymap)
85✔
2836
end
2837

2838
function deactivate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal)
732✔
2839
    clear_input_area(termbuf, s)
732✔
2840
    return s
732✔
2841
end
2842

2843
function activate(p::TextInterface, s::ModeState, termbuf::AbstractTerminal, term::TextTerminal)
1,137✔
2844
    s.ias = InputAreaState(0, 0)
1,137✔
2845
    refresh_line(s, termbuf)
1,137✔
2846
    nothing
1,137✔
2847
end
2848

2849
function activate(p::TextInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal)
405✔
2850
    @assert p == mode(s)
405✔
2851
    activate(p, state(s), termbuf, term)
405✔
2852
    nothing
405✔
2853
end
2854
activate(m::ModalInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal) =
405✔
2855
    activate(mode(s), s, termbuf, term)
2856

2857
commit_changes(t::UnixTerminal, termbuf::TerminalBuffer) = (write(t, take!(termbuf.out_stream)); nothing)
732✔
2858

2859
function transition(f::Function, s::MIState, newmode::Union{TextInterface,Symbol})
831✔
2860
    cancel_beep(s)
831✔
2861
    if newmode === :abort
831✔
2862
        s.aborted = true
72✔
2863
        return
72✔
2864
    end
2865
    if newmode === :reset
759✔
2866
        reset_state(s)
27✔
2867
        return
27✔
2868
    end
2869
    if !haskey(s.mode_state, newmode)
732✔
UNCOV
2870
        s.mode_state[newmode] = init_state(terminal(s), newmode)
×
2871
    end
2872
    termbuf = TerminalBuffer(IOBuffer())
732✔
2873
    t = terminal(s)
732✔
2874
    s.mode_state[mode(s)] = deactivate(mode(s), state(s), termbuf, t)
732✔
2875
    s.current_mode = newmode
1,464✔
2876
    f()
732✔
2877
    activate(newmode, state(s, newmode), termbuf, t)
732✔
2878
    commit_changes(t, termbuf)
732✔
2879
    nothing
732✔
2880
end
2881
transition(s::MIState, mode::Union{TextInterface,Symbol}) = transition((args...)->nothing, s, mode)
977✔
2882

2883
function reset_state(s::PromptState)
1,520✔
2884
    if s.input_buffer.size != 0
1,520✔
2885
        s.input_buffer.size = 0
27✔
2886
        s.input_buffer.ptr = 1
27✔
2887
    end
2888
    empty_undo(s)
9,005✔
2889
    deactivate_region(s)
1,520✔
2890
    ias = InputAreaState(0, 0)
1,520✔
2891
    s.ias = ias
1,520✔
2892
    return ias
1,520✔
2893
end
2894

2895
function reset_state(s::MIState)
389✔
2896
    for (mode, state) in s.mode_state
778✔
2897
        reset_state(state)
1,909✔
2898
    end
1,909✔
2899
end
2900

2901
const default_keymap_dict = keymap([default_keymap, escape_defaults])
2902

2903
function Prompt(prompt
697✔
2904
    ;
2905
    prompt_prefix = "",
2906
    prompt_suffix = "",
2907
    output_prefix = "",
2908
    output_prefix_prefix = "",
2909
    output_prefix_suffix = "",
2910
    keymap_dict = default_keymap_dict,
2911
    repl = nothing,
2912
    complete = EmptyCompletionProvider(),
2913
    on_enter = default_enter_cb,
2914
    on_done = ()->nothing,
2915
    hist = EmptyHistoryProvider(),
2916
    sticky = false,
2917
    styling_passes = StylingPass[])
2918

2919
    return Prompt(prompt, prompt_prefix, prompt_suffix, output_prefix, output_prefix_prefix, output_prefix_suffix,
370✔
2920
                   keymap_dict, repl, complete, on_enter, on_done, hist, sticky, styling_passes)
2921
end
2922

UNCOV
2923
run_interface(::Prompt) = nothing
×
2924

2925
init_state(terminal, prompt::Prompt) =
457✔
2926
    PromptState(terminal, prompt, IOBuffer(), :off, nothing, IOBuffer[], 1, InputAreaState(1, 1),
2927
                #=indent(spaces)=# -1, Threads.SpinLock(), 0.0, -Inf, nothing)
2928

2929
function init_state(terminal, m::ModalInterface)
202✔
2930
    s = MIState(m, Main, m.modes[1], false, IdDict{Any,Any}())
202✔
2931
    for mode in m.modes
202✔
2932
        s.mode_state[mode] = init_state(terminal, mode)
533✔
2933
    end
533✔
2934
    return s
202✔
2935
end
2936

2937

2938
function run_interface(terminal::TextTerminal, m::ModalInterface, s::MIState=init_state(terminal, m))
76✔
2939
    while !s.aborted
486✔
2940
        buf, ok, suspend = prompt!(terminal, m, s)
405✔
2941
        while suspend
404✔
2942
            @static if Sys.isunix(); ccall(:jl_repl_raise_sigtstp, Cint, ()); end
UNCOV
2943
            buf, ok, suspend = prompt!(terminal, m, s)
×
UNCOV
2944
        end
×
2945
        Base.invokelatest(mode(state(s)).on_done, s, buf, ok)
404✔
2946
    end
404✔
2947
end
2948

2949
buffer(s) = _buffer(s)::IOBuffer
25,439✔
2950
_buffer(s::PromptState) = s.input_buffer
16,953✔
2951
_buffer(s::PrefixSearchState) = s.response_buffer
510✔
2952
_buffer(s::IOBuffer) = s
7,976✔
2953

2954
position(s::Union{MIState,ModeState}) = position(buffer(s))
298✔
2955

2956
function empty_undo(s::PromptState)
2957
    empty!(s.undo_buffers)
9,302✔
2958
    s.undo_idx = 1
1,697✔
2959
    nothing
1,697✔
2960
end
2961

UNCOV
2962
empty_undo(s) = nothing
×
2963

2964
function push_undo(s::PromptState, advance::Bool=true)
264✔
2965
    resize!(s.undo_buffers, s.undo_idx)
17,090✔
2966
    s.undo_buffers[end] = copy(s.input_buffer)
8,569✔
2967
    advance && (s.undo_idx += 1)
8,569✔
2968
    nothing
8,569✔
2969
end
2970

UNCOV
2971
push_undo(s) = nothing
×
2972

2973
# must be called after a push_undo
2974
function pop_undo(s::PromptState)
2975
    pop!(s.undo_buffers)
15✔
2976
    s.undo_idx -= 1
15✔
2977
    nothing
15✔
2978
end
2979

2980
function edit_undo!(s::MIState)
108✔
2981
    set_action!(s, :edit_undo!)
108✔
2982
    s.last_action ∉ (:edit_redo!, :edit_undo!) && push_undo(s, false)
108✔
2983
    if !edit_undo!(state(s))
108✔
2984
        beep(s)
3✔
2985
        return :ignore
3✔
2986
    end
2987
    return nothing
105✔
2988
end
2989

2990
function edit_undo!(s::PromptState)
108✔
2991
    s.undo_idx > 1 || return false
111✔
2992
    s.input_buffer = s.undo_buffers[s.undo_idx -=1]
105✔
2993
    refresh_line(s)
105✔
2994
    return true
105✔
2995
end
UNCOV
2996
edit_undo!(s) = nothing
×
2997

2998
function edit_redo!(s::MIState)
18✔
2999
    set_action!(s, :edit_redo!)
18✔
3000
    if s.last_action ∉ (:edit_redo!, :edit_undo!) || !edit_redo!(state(s))
36✔
3001
        beep(s)
3✔
3002
        return :ignore
3✔
3003
    end
3004
    return nothing
15✔
3005
end
3006

3007
function edit_redo!(s::PromptState)
18✔
3008
    s.undo_idx < length(s.undo_buffers) || return false
21✔
3009
    s.input_buffer = s.undo_buffers[s.undo_idx += 1]
15✔
3010
    refresh_line(s)
15✔
3011
    return true
15✔
3012
end
UNCOV
3013
edit_redo!(s) = nothing
×
3014

3015
keymap(s::PromptState, prompt::Prompt) = prompt.keymap_dict
8,670✔
3016
keymap_data(s::PromptState, prompt::Prompt) = prompt.repl
8,670✔
3017
keymap(ms::MIState, m::ModalInterface) = keymap(state(ms), mode(ms))
8,673✔
3018
keymap_data(ms::MIState, m::ModalInterface) = keymap_data(state(ms), mode(ms))
8,673✔
3019

3020
function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_state(term, prompt))
405✔
3021
    Base.reseteof(term)
405✔
3022
    t1 = Threads.@spawn :interactive while true
810✔
3023
        wait(s.async_channel)
405✔
3024
        status = @lock s.line_modify_lock begin
405✔
3025
            fcn = take!(s.async_channel)
810✔
3026
            fcn(s)
405✔
3027
        end
3028
        status ∈ (:ok, :ignore) || break
405✔
UNCOV
3029
    end
×
3030
    raw!(term, true)
405✔
3031
    enable_bracketed_paste(term)
405✔
3032
    try
405✔
3033
        activate(prompt, s, term, term)
405✔
3034
        # Notify that prompt is ready for input
3035
        if s.prompt_ready_event !== nothing
405✔
UNCOV
3036
            notify(s.prompt_ready_event)
×
3037
        end
3038
        old_state = mode(s)
405✔
3039
        # spawn this because the main repl task is sticky (due to use of @async and _wait2)
3040
        # and we want to not block typing when the repl task thread is busy
3041
        t2 = Threads.@spawn :interactive while true
810✔
3042
            eof(term) || peek(term) # wait before locking but don't consume
17,340✔
3043
            @lock s.line_modify_lock begin
8,673✔
3044
                s.n_keys_pressed += 1
8,673✔
3045
                kmap = keymap(s, prompt)
8,673✔
3046
                fcn = match_input(kmap, s)
17,346✔
3047
                kdata = keymap_data(s, prompt)
8,673✔
3048
                s.current_action = :unknown # if the to-be-run action doesn't update this field,
8,673✔
3049
                                            # :unknown will be recorded in the last_action field
3050
                local status
3051
                # errors in keymaps shouldn't cause the REPL to fail, so wrap in a
3052
                # try/catch block
3053
                try
8,673✔
3054
                    status = fcn(s, kdata)
8,673✔
3055
                catch e
UNCOV
3056
                    @error "Error in the keymap" exception=e,catch_backtrace()
×
3057
                    # try to cleanup and get `s` back to its original state before returning
UNCOV
3058
                    transition(s, :reset)
×
UNCOV
3059
                    transition(s, old_state)
×
UNCOV
3060
                    status = :done
×
3061
                end
3062
                status !== :ignore && (s.last_action = s.current_action)
8,673✔
3063
                if status === :abort
8,673✔
3064
                    s.aborted = true
75✔
3065
                    return buffer(s), false, false
75✔
3066
                elseif status === :done
8,598✔
3067
                    return buffer(s), true, false
330✔
3068
                elseif status === :suspend
8,268✔
UNCOV
3069
                    if Sys.isunix()
×
3070
                        return buffer(s), true, true
3071
                    end
3072
                else
3073
                    @assert status ∈ (:ok, :ignore)
8,268✔
3074
                end
3075
            end
3076
        end
8,268✔
3077
        return fetch(t2)
405✔
3078
    finally
3079
        put!(s.async_channel, Returns(:done))
810✔
3080
        wait(t1)
405✔
3081
        raw!(term, false) && disable_bracketed_paste(term)
405✔
3082
    end
3083
    # unreachable
3084
end
3085

3086

3087
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