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

JuliaLang / julia / #38002

06 Feb 2025 06:14AM UTC coverage: 20.322% (-2.4%) from 22.722%
#38002

push

local

web-flow
bpart: Fully switch to partitioned semantics (#57253)

This is the final PR in the binding partitions series (modulo bugs and
tweaks), i.e. it closes #54654 and thus closes #40399, which was the
original design sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in
particular allows struct redefinitions. This thus closes
timholy/Revise.jl#18 and also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several
related issues in the bugtracker, so this closes #14055 closes #44604
closes #46354 closes #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus closes
#53958 closes #54733 - however, this is not activated yet for
performance reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
slightly differently than the old semantics absent some other task going
around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
permissible under the old semantics if the bindings happened to be
resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no
binding", plus slightly more exotic corner cases around conflicts

2. Weakly declared bindin... (continued)

11 of 111 new or added lines in 7 files covered. (9.91%)

1273 existing lines in 68 files now uncovered.

9908 of 48755 relevant lines covered (20.32%)

105126.48 hits per line

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

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

3
module REPLCompletions
4

5
export completions, shell_completions, bslash_completions, completion_text, named_completion
6

7
using Core: Const
8
# We want to insulate the REPLCompletion module from any changes the user may
9
# make to the compiler, since it runs by default and the system becomes unusable
10
# if it breaks.
11
const CC = Base.Compiler
12
using Base.Meta
13
using Base: propertynames, something, IdSet
14
using Base.Filesystem: _readdirx
15

16
using ..REPL.LineEdit: NamedCompletion
17

18
abstract type Completion end
19

20
struct TextCompletion <: Completion
21
    text::String
22
end
23

24
struct KeywordCompletion <: Completion
25
    keyword::String
26
end
27

28
struct KeyvalCompletion <: Completion
29
    keyval::String
30
end
31

32
struct PathCompletion <: Completion
33
    path::String
34
end
35

36
struct ModuleCompletion <: Completion
37
    parent::Module
38
    mod::String
39
end
40

41
struct PackageCompletion <: Completion
42
    package::String
43
end
44

45
struct PropertyCompletion <: Completion
46
    value
47
    property::Symbol
48
end
49

50
struct FieldCompletion <: Completion
51
    typ::DataType
52
    field::Symbol
53
end
54

55
struct MethodCompletion <: Completion
56
    tt # may be used by an external consumer to infer return type, etc.
57
    method::Method
58
    MethodCompletion(@nospecialize(tt), method::Method) = new(tt, method)
×
59
end
60

61
struct BslashCompletion <: Completion
62
    completion::String # what is actually completed, for example "\trianglecdot"
63
    name::String # what is displayed, for example "â—¬ \trianglecdot"
64
end
65
BslashCompletion(completion::String) = BslashCompletion(completion, completion)
×
66

67
struct ShellCompletion <: Completion
68
    text::String
69
end
70

71
struct DictCompletion <: Completion
72
    dict::AbstractDict
73
    key::String
74
end
75

76
struct KeywordArgumentCompletion <: Completion
77
    kwarg::String
78
end
79

80
# interface definition
81
function Base.getproperty(c::Completion, name::Symbol)
×
82
    if name === :text
×
83
        return getfield(c, :text)::String
×
84
    elseif name === :keyword
×
85
        return getfield(c, :keyword)::String
×
86
    elseif name === :path
×
87
        return getfield(c, :path)::String
×
88
    elseif name === :parent
×
89
        return getfield(c, :parent)::Module
×
90
    elseif name === :mod
×
91
        return getfield(c, :mod)::String
×
92
    elseif name === :package
×
93
        return getfield(c, :package)::String
×
94
    elseif name === :property
×
95
        return getfield(c, :property)::Symbol
×
96
    elseif name === :field
×
97
        return getfield(c, :field)::Symbol
×
98
    elseif name === :method
×
99
        return getfield(c, :method)::Method
×
100
    elseif name === :bslash
×
101
        return getfield(c, :bslash)::String
×
102
    elseif name === :text
×
103
        return getfield(c, :text)::String
×
104
    elseif name === :key
×
105
        return getfield(c, :key)::String
×
106
    elseif name === :kwarg
×
107
        return getfield(c, :kwarg)::String
×
108
    end
109
    return getfield(c, name)
×
110
end
111

112
_completion_text(c::TextCompletion) = c.text
×
113
_completion_text(c::KeywordCompletion) = c.keyword
×
114
_completion_text(c::KeyvalCompletion) = c.keyval
×
115
_completion_text(c::PathCompletion) = c.path
×
116
_completion_text(c::ModuleCompletion) = c.mod
×
117
_completion_text(c::PackageCompletion) = c.package
×
118
_completion_text(c::PropertyCompletion) = sprint(Base.show_sym, c.property)
×
119
_completion_text(c::FieldCompletion) = sprint(Base.show_sym, c.field)
×
120
_completion_text(c::MethodCompletion) = repr(c.method)
×
121
_completion_text(c::ShellCompletion) = c.text
×
122
_completion_text(c::DictCompletion) = c.key
×
123
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='
×
124

125
completion_text(c) = _completion_text(c)::String
×
126

127
named_completion(c::BslashCompletion) = NamedCompletion(c.completion, c.name)
×
128

129
function named_completion(c)
×
130
    text = completion_text(c)::String
×
131
    return NamedCompletion(text, text)
×
132
end
133

134
named_completion_completion(c) = named_completion(c).completion::String
×
135

136
const Completions = Tuple{Vector{Completion}, UnitRange{Int}, Bool}
137

138
function completes_global(x, name)
×
139
    return startswith(x, name) && !('#' in x)
×
140
end
141

142
function appendmacro!(syms, macros, needle, endchar)
×
143
    for macsym in macros
×
144
        s = String(macsym)
×
145
        if endswith(s, needle)
×
146
            from = nextind(s, firstindex(s))
×
147
            to = prevind(s, sizeof(s)-sizeof(needle)+1)
×
148
            push!(syms, s[from:to]*endchar)
×
149
        end
150
    end
×
151
end
152

153
function append_filtered_mod_names!(ffunc::Function, suggestions::Vector{Completion},
×
154
                                    mod::Module, name::String, complete_internal_only::Bool)
155
    imported = usings = !complete_internal_only
×
156
    ssyms = names(mod; all=true, imported, usings)
×
157
    filter!(ffunc, ssyms)
×
158
    macros = filter(x -> startswith(String(x), "@" * name), ssyms)
×
159

160
    # don't complete string and command macros when the input matches the internal name like `r_` to `r"`
161
    if !startswith(name, "@")
×
162
        filter!(macros) do m
×
163
            s = String(m)
×
164
            if endswith(s, "_str") || endswith(s, "_cmd")
×
165
                occursin(name, first(s, length(s)-4))
×
166
            else
167
                true
×
168
            end
169
        end
170
    end
171

172
    syms = String[sprint((io,s)->Base.show_sym(io, s; allow_macroname=true), s) for s in ssyms if completes_global(String(s), name)]
×
173
    appendmacro!(syms, macros, "_str", "\"")
×
174
    appendmacro!(syms, macros, "_cmd", "`")
×
175
    for sym in syms
×
176
        push!(suggestions, ModuleCompletion(mod, sym))
×
177
    end
×
178
    return suggestions
×
179
end
180

181
# REPL Symbol Completions
182
function complete_symbol!(suggestions::Vector{Completion},
×
183
                          @nospecialize(prefix), name::String, context_module::Module;
184
                          complete_modules_only::Bool=false,
185
                          shift::Bool=false)
186
    local mod, t, val
×
187
    complete_internal_only = false
×
188
    if prefix !== nothing
×
189
        res = repl_eval_ex(prefix, context_module)
×
190
        res === nothing && return Completion[]
×
191
        if res isa Const
×
192
            val = res.val
×
193
            if isa(val, Module)
×
194
                mod = val
×
195
                if !shift
×
196
                    # when module is explicitly accessed, show internal bindings that are
197
                    # defined by the module, unless shift key is pressed
198
                    complete_internal_only = true
×
199
                end
200
            else
201
                t = typeof(val)
×
202
            end
203
        else
204
            t = CC.widenconst(res)
×
205
        end
206
    else
207
        mod = context_module
×
208
    end
209

210
    if @isdefined(mod) # lookup names available within the module
×
211
        let modname = nameof(mod),
×
212
            is_main = mod===Main
213
            append_filtered_mod_names!(suggestions, mod, name, complete_internal_only) do s::Symbol
×
214
                if Base.isdeprecated(mod, s)
×
215
                    return false
×
216
                elseif s === modname
×
217
                    return false # exclude `Main.Main.Main`, etc.
×
218
                elseif complete_modules_only && !completes_module(mod, s)
×
219
                    return false
×
220
                elseif is_main && s === :MainInclude
×
221
                    return false
×
222
                end
223
                return true
×
224
            end
225
        end
226
    elseif @isdefined(val) # looking for a property of an instance
×
227
        try
×
228
            for property in propertynames(val, false)
×
229
                # TODO: support integer arguments (#36872)
230
                if property isa Symbol && startswith(string(property), name)
×
231
                    push!(suggestions, PropertyCompletion(val, property))
×
232
                end
233
            end
×
234
        catch
×
235
        end
236
    elseif @isdefined(t) && field_completion_eligible(t)
×
237
        # Looking for a member of a type
238
        add_field_completions!(suggestions, name, t)
×
239
    end
240
    return suggestions
×
241
end
242

NEW
243
completes_module(mod::Module, x::Symbol) = isdefined(mod, x) && isa(getglobal(mod, x), Module)
×
244

245
function add_field_completions!(suggestions::Vector{Completion}, name::String, @nospecialize(t))
×
246
    if isa(t, Union)
×
247
        add_field_completions!(suggestions, name, t.a)
×
248
        add_field_completions!(suggestions, name, t.b)
×
249
    else
250
        @assert isconcretetype(t)
×
251
        fields = fieldnames(t)
×
252
        for field in fields
×
253
            isa(field, Symbol) || continue # Tuple type has ::Int field name
×
254
            s = string(field)
×
255
            if startswith(s, name)
×
256
                push!(suggestions, FieldCompletion(t, field))
×
257
            end
258
        end
×
259
    end
260
end
261

262
const GENERIC_PROPERTYNAMES_METHOD = which(propertynames, (Any,))
263

264
function field_completion_eligible(@nospecialize t)
×
265
    if isa(t, Union)
×
266
        return field_completion_eligible(t.a) && field_completion_eligible(t.b)
×
267
    end
268
    isconcretetype(t) || return false
×
269
    # field completion is correct only when `getproperty` fallbacks to `getfield`
270
    match = Base._which(Tuple{typeof(propertynames),t}; raise=false)
×
271
    match === nothing && return false
×
272
    return match.method === GENERIC_PROPERTYNAMES_METHOD
×
273
end
274

275
function complete_from_list!(suggestions::Vector{Completion}, T::Type, list::Vector{String}, s::String)
×
276
    r = searchsorted(list, s)
×
277
    i = first(r)
×
278
    n = length(list)
×
279
    while i <= n && startswith(list[i],s)
×
280
        r = first(r):i
×
281
        i += 1
×
282
    end
×
283
    for kw in list[r]
×
284
        push!(suggestions, T(kw))
×
285
    end
×
286
    return suggestions
×
287
end
288

289
const sorted_keywords = [
290
    "abstract type", "baremodule", "begin", "break", "catch", "ccall",
291
    "const", "continue", "do", "else", "elseif", "end", "export",
292
    "finally", "for", "function", "global", "if", "import",
293
    "let", "local", "macro", "module", "mutable struct",
294
    "primitive type", "quote", "return", "struct",
295
    "try", "using", "while"]
296

297
complete_keyword!(suggestions::Vector{Completion}, s::String) =
×
298
    complete_from_list!(suggestions, KeywordCompletion, sorted_keywords, s)
299

300
const sorted_keyvals = ["false", "true"]
301

302
complete_keyval!(suggestions::Vector{Completion}, s::String) =
×
303
    complete_from_list!(suggestions, KeyvalCompletion, sorted_keyvals, s)
304

305
function do_raw_escape(s)
×
306
    # escape_raw_string with delim='`' and ignoring the rule for the ending \
307
    return replace(s, r"(\\+)`" => s"\1\\`")
×
308
end
309
function do_shell_escape(s)
×
310
    return Base.shell_escape_posixly(s)
×
311
end
312
function do_string_escape(s)
×
313
    return escape_string(s, ('\"','$'))
×
314
end
315

316
const PATH_cache_lock = Base.ReentrantLock()
317
const PATH_cache = Set{String}()
318
PATH_cache_task::Union{Task,Nothing} = nothing # used for sync in tests
319
next_cache_update::Float64 = 0.0
320
function maybe_spawn_cache_PATH()
×
321
    global PATH_cache_task, next_cache_update
×
322
    @lock PATH_cache_lock begin
×
323
        PATH_cache_task isa Task && !istaskdone(PATH_cache_task) && return
×
324
        time() < next_cache_update && return
×
325
        PATH_cache_task = Threads.@spawn begin
×
326
            REPLCompletions.cache_PATH()
×
327
            @lock PATH_cache_lock PATH_cache_task = nothing # release memory when done
×
328
        end
329
        Base.errormonitor(PATH_cache_task)
×
330
    end
331
end
332

333
# caches all reachable files in PATH dirs
334
function cache_PATH()
×
335
    path = get(ENV, "PATH", nothing)
×
336
    path isa String || return
×
337

338
    global next_cache_update
×
339

340
    # Calling empty! on PATH_cache would be annoying for async typing hints as completions would temporarily disappear.
341
    # So keep track of what's added this time and at the end remove any that didn't appear this time from the global cache.
342
    this_PATH_cache = Set{String}()
×
343

344
    @debug "caching PATH files" PATH=path
×
345
    pathdirs = split(path, @static Sys.iswindows() ? ";" : ":")
×
346

347
    next_yield_time = time() + 0.01
×
348

349
    t = @elapsed for pathdir in pathdirs
×
350
        actualpath = try
×
351
            realpath(pathdir)
×
352
        catch ex
353
            ex isa Base.IOError || rethrow()
×
354
            # Bash doesn't expect every folder in PATH to exist, so neither shall we
355
            continue
×
356
        end
357

358
        if actualpath != pathdir && in(actualpath, pathdirs)
×
359
            # Remove paths which (after resolving links) are in the env path twice.
360
            # Many distros eg. point /bin to /usr/bin but have both in the env path.
361
            continue
×
362
        end
363

364
        path_entries = try
×
365
            _readdirx(pathdir)
×
366
        catch e
367
            # Bash allows dirs in PATH that can't be read, so we should as well.
368
            if isa(e, Base.IOError) || isa(e, Base.ArgumentError)
×
369
                continue
×
370
            else
371
                # We only handle IOError and ArgumentError here
372
                rethrow()
×
373
            end
374
        end
375
        for entry in path_entries
×
376
            # In a perfect world, we would filter on whether the file is executable
377
            # here, or even on whether the current user can execute the file in question.
378
            try
×
379
                if isfile(entry)
×
380
                    @lock PATH_cache_lock push!(PATH_cache, entry.name)
×
381
                    push!(this_PATH_cache, entry.name)
×
382
                end
383
            catch e
384
                # `isfile()` can throw in rare cases such as when probing a
385
                # symlink that points to a file within a directory we do not
386
                # have read access to.
387
                if isa(e, Base.IOError)
×
388
                    continue
×
389
                else
390
                    rethrow()
×
391
                end
392
            end
393
            if time() >= next_yield_time
×
394
                yield() # to avoid blocking typing when -t1
×
395
                next_yield_time = time() + 0.01
×
396
            end
397
        end
×
398
    end
×
399

400
    @lock PATH_cache_lock begin
×
401
        intersect!(PATH_cache, this_PATH_cache) # remove entries from PATH_cache that weren't found this time
×
402
        next_cache_update = time() + 10 # earliest next update can run is 10s after
×
403
    end
404

405
    @debug "caching PATH files took $t seconds" length(pathdirs) length(PATH_cache)
×
406
    return PATH_cache
×
407
end
408

409
function complete_path(path::AbstractString;
×
410
                       use_envpath=false,
411
                       shell_escape=false,
412
                       raw_escape=false,
413
                       string_escape=false,
414
                       contract_user=false)
415
    @assert !(shell_escape && string_escape)
×
416
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
417
        # if the path is just "~", don't consider the expanded username as a prefix
418
        if path == "~"
×
419
            dir, prefix = homedir(), ""
×
420
        else
421
            dir, prefix = splitdir(homedir() * path[2:end])
×
422
        end
423
    else
424
        dir, prefix = splitdir(path)
×
425
    end
426
    entries = try
×
427
        if isempty(dir)
×
428
            _readdirx()
×
429
        elseif isdir(dir)
×
430
            _readdirx(dir)
×
431
        else
432
            return Completion[], dir, false
×
433
        end
434
    catch ex
435
        ex isa Base.IOError || rethrow()
×
436
        return Completion[], dir, false
×
437
    end
438

439
    matches = Set{String}()
×
440
    for entry in entries
×
441
        if startswith(entry.name, prefix)
×
442
            is_dir = try isdir(entry) catch ex; ex isa Base.IOError ? false : rethrow() end
×
443
            push!(matches, is_dir ? entry.name * "/" : entry.name)
×
444
        end
445
    end
×
446

447
    if use_envpath && isempty(dir)
×
448
        # Look for files in PATH as well. These are cached in `cache_PATH` in an async task to not block typing.
449
        # If we cannot get lock because its still caching just pass over this so that typing isn't laggy.
450
        maybe_spawn_cache_PATH() # only spawns if enough time has passed and the previous caching task has completed
×
451
        @lock PATH_cache_lock begin
×
452
            for file in PATH_cache
×
453
                startswith(file, prefix) && push!(matches, file)
×
454
            end
×
455
        end
456
    end
457

458
    matches = ((shell_escape ? do_shell_escape(s) : string_escape ? do_string_escape(s) : s) for s in matches)
×
459
    matches = ((raw_escape ? do_raw_escape(s) : s) for s in matches)
×
460
    matches = Completion[PathCompletion(contract_user ? contractuser(s) : s) for s in matches]
×
461
    return matches, dir, !isempty(matches)
×
462
end
463

464
function complete_path(path::AbstractString,
×
465
                       pos::Int;
466
                       use_envpath=false,
467
                       shell_escape=false,
468
                       string_escape=false,
469
                       contract_user=false)
470
    ## TODO: enable this depwarn once Pkg is fixed
471
    #Base.depwarn("complete_path with pos argument is deprecated because the return value [2] is incorrect to use", :complete_path)
472
    paths, dir, success = complete_path(path; use_envpath, shell_escape, string_escape)
×
473
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
474
        # if the path is just "~", don't consider the expanded username as a prefix
475
        if path == "~"
×
476
            dir, prefix = homedir(), ""
×
477
        else
478
            dir, prefix = splitdir(homedir() * path[2:end])
×
479
        end
480
    else
481
        dir, prefix = splitdir(path)
×
482
    end
483
    startpos = pos - lastindex(prefix) + 1
×
484
    Sys.iswindows() && map!(paths, paths) do c::PathCompletion
×
485
        # emulation for unnecessarily complicated return value, since / is a
486
        # perfectly acceptable path character which does not require quoting
487
        # but is required by Pkg's awkward parser handling
488
        return endswith(c.path, "/") ? PathCompletion(chop(c.path) * "\\\\") : c
×
489
    end
490
    return paths, startpos:pos, success
×
491
end
492

493
function complete_expanduser(path::AbstractString, r)
×
494
    expanded =
×
495
        try expanduser(path)
×
496
        catch e
497
            e isa ArgumentError || rethrow()
×
498
            path
×
499
        end
500
    return Completion[PathCompletion(expanded)], r, path != expanded
×
501
end
502

503
# Returns a range that includes the method name in front of the first non
504
# closed start brace from the end of the string.
505
function find_start_brace(s::AbstractString; c_start='(', c_end=')')
×
506
    r = reverse(s)
×
507
    i = firstindex(r)
×
508
    braces = in_comment = 0
×
509
    in_single_quotes = in_double_quotes = in_back_ticks = false
×
510
    num_single_quotes_in_string = count('\'', s)
×
511
    while i <= ncodeunits(r)
×
512
        c, i = iterate(r, i)
×
513
        if c == '#' && i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
514
            c, i = iterate(r, i) # consume '='
×
515
            new_comments = 1
×
516
            # handle #=#=#=#, by counting =# pairs
517
            while i <= ncodeunits(r) && iterate(r, i)[1] == '#'
×
518
                c, i = iterate(r, i) # consume '#'
×
519
                iterate(r, i)[1] == '=' || break
×
520
                c, i = iterate(r, i) # consume '='
×
521
                new_comments += 1
×
522
            end
×
523
            if c == '='
×
524
                in_comment += new_comments
×
525
            else
526
                in_comment -= new_comments
×
527
            end
528
        elseif !in_single_quotes && !in_double_quotes && !in_back_ticks && in_comment == 0
×
529
            if c == c_start
×
530
                braces += 1
×
531
            elseif c == c_end
×
532
                braces -= 1
×
533
            elseif c == '\'' && num_single_quotes_in_string % 2 == 0
×
534
                # ' can be a transpose too, so check if there are even number of 's in the string
535
                # TODO: This probably needs to be more robust
536
                in_single_quotes = true
×
537
            elseif c == '"'
×
538
                in_double_quotes = true
×
539
            elseif c == '`'
×
540
                in_back_ticks = true
×
541
            end
542
        else
543
            if in_single_quotes &&
×
544
                c == '\'' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
545
                in_single_quotes = false
×
546
            elseif in_double_quotes &&
×
547
                c == '"' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
548
                in_double_quotes = false
×
549
            elseif in_back_ticks &&
×
550
                c == '`' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
551
                in_back_ticks = false
×
552
            elseif in_comment > 0 &&
×
553
                c == '=' && i <= ncodeunits(r) && iterate(r, i)[1] == '#'
554
                # handle =#=#=#=, by counting #= pairs
555
                c, i = iterate(r, i) # consume '#'
×
556
                old_comments = 1
×
557
                while i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
558
                    c, i = iterate(r, i) # consume '='
×
559
                    iterate(r, i)[1] == '#' || break
×
560
                    c, i = iterate(r, i) # consume '#'
×
561
                    old_comments += 1
×
562
                end
×
563
                if c == '#'
×
564
                    in_comment -= old_comments
×
565
                else
566
                    in_comment += old_comments
×
567
                end
568
            end
569
        end
570
        braces == 1 && break
×
571
    end
×
572
    braces != 1 && return 0:-1, -1
×
573
    method_name_end = reverseind(s, i)
×
574
    startind = nextind(s, something(findprev(in(non_identifier_chars), s, method_name_end), 0))::Int
×
575
    return (startind:lastindex(s), method_name_end)
×
576
end
577

578
struct REPLCacheToken end
579

580
struct REPLInterpreter <: CC.AbstractInterpreter
581
    limit_aggressive_inference::Bool
582
    world::UInt
583
    inf_params::CC.InferenceParams
584
    opt_params::CC.OptimizationParams
585
    inf_cache::Vector{CC.InferenceResult}
586
    function REPLInterpreter(limit_aggressive_inference::Bool=false;
×
587
                             world::UInt = Base.get_world_counter(),
588
                             inf_params::CC.InferenceParams = CC.InferenceParams(;
589
                                 aggressive_constant_propagation=true),
590
                             opt_params::CC.OptimizationParams = CC.OptimizationParams(),
591
                             inf_cache::Vector{CC.InferenceResult} = CC.InferenceResult[])
592
        return new(limit_aggressive_inference, world, inf_params, opt_params, inf_cache)
×
593
    end
594
end
595
CC.InferenceParams(interp::REPLInterpreter) = interp.inf_params
×
596
CC.OptimizationParams(interp::REPLInterpreter) = interp.opt_params
×
597
CC.get_inference_world(interp::REPLInterpreter) = interp.world
×
598
CC.get_inference_cache(interp::REPLInterpreter) = interp.inf_cache
×
599
CC.cache_owner(::REPLInterpreter) = REPLCacheToken()
×
600

601
# REPLInterpreter is only used for type analysis, so it should disable optimization entirely
602
CC.may_optimize(::REPLInterpreter) = false
×
603

604
# REPLInterpreter doesn't need any sources to be cached, so discard them aggressively
605
CC.transform_result_for_cache(::REPLInterpreter, ::CC.InferenceResult) = nothing
×
606

607
# REPLInterpreter analyzes a top-level frame, so better to not bail out from it
608
CC.bail_out_toplevel_call(::REPLInterpreter, ::CC.InferenceLoopState, ::CC.InferenceState) = false
×
609

610
# `REPLInterpreter` aggressively resolves global bindings to enable reasonable completions
611
# for lines like `Mod.a.|` (where `|` is the cursor position).
612
# Aggressive binding resolution poses challenges for the inference cache validation
613
# (until https://github.com/JuliaLang/julia/issues/40399 is implemented).
614
# To avoid the cache validation issues, `REPLInterpreter` only allows aggressive binding
615
# resolution for top-level frame representing REPL input code and for child uncached frames
616
# that are constant propagated from the top-level frame ("repl-frame"s). This works, even if
617
# those global bindings are not constant and may be mutated in the future, since:
618
# a.) "repl-frame"s are never cached, and
619
# b.) mutable values are never observed by any cached frames.
620
#
621
# `REPLInterpreter` also aggressively concrete evaluate `:inconsistent` calls within
622
# "repl-frame" to provide reasonable completions for lines like `Ref(Some(42))[].|`.
623
# Aggressive concrete evaluation allows us to get accurate type information about complex
624
# expressions that otherwise can not be constant folded, in a safe way, i.e. it still
625
# doesn't evaluate effectful expressions like `pop!(xs)`.
626
# Similarly to the aggressive binding resolution, aggressive concrete evaluation doesn't
627
# present any cache validation issues because "repl-frame" is never cached.
628

629
# `REPLInterpreter` is specifically used by `repl_eval_ex`, where all top-level frames are
630
# `repl_frame` always. However, this assumption wouldn't stand if `REPLInterpreter` were to
631
# be employed, for instance, by `typeinf_ext_toplevel`.
632
is_repl_frame(sv::CC.InferenceState) = sv.linfo.def isa Module && sv.cache_mode === CC.CACHE_MODE_NULL
×
633

634
function is_call_graph_uncached(sv::CC.InferenceState)
×
635
    CC.is_cached(sv) && return false
×
636
    parent = CC.frame_parent(sv)
×
637
    parent === nothing && return true
×
638
    return is_call_graph_uncached(parent::CC.InferenceState)
×
639
end
640

641
# aggressive global binding resolution within `repl_frame`
642
function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef, bailed::Bool,
×
643
                                    sv::CC.InferenceState)
644
    # Ignore saw_latestworld
645
    partition = CC.abstract_eval_binding_partition!(interp, g, sv)
×
646
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
×
647
        if CC.is_defined_const_binding(CC.binding_kind(partition))
×
648
            return Pair{CC.RTEffects, Union{Nothing, Core.BindingPartition}}(
×
649
                CC.RTEffects(Const(CC.partition_restriction(partition)), Union{}, CC.EFFECTS_TOTAL), partition)
650
        else
651
            b = convert(Core.Binding, g)
×
652
            if CC.binding_kind(partition) == CC.BINDING_KIND_GLOBAL && isdefined(b, :value)
×
653
                return Pair{CC.RTEffects, Union{Nothing, Core.BindingPartition}}(
×
654
                    CC.RTEffects(Const(b.value), Union{}, CC.EFFECTS_TOTAL), partition)
655
            end
656
        end
657
        return Pair{CC.RTEffects, Union{Nothing, Core.BindingPartition}}(
×
658
            CC.RTEffects(Union{}, UndefVarError, CC.EFFECTS_THROWS), partition)
659
    end
660
    return @invoke CC.abstract_eval_globalref(interp::CC.AbstractInterpreter, g::GlobalRef, bailed::Bool,
×
661
                                              sv::CC.InferenceState)
662
end
663

664
function is_repl_frame_getproperty(sv::CC.InferenceState)
×
665
    def = sv.linfo.def
×
666
    def isa Method || return false
×
667
    def.name === :getproperty || return false
×
668
    CC.is_cached(sv) && return false
×
669
    return is_repl_frame(CC.frame_parent(sv))
×
670
end
671

672
# aggressive global binding resolution for `getproperty(::Module, ::Symbol)` calls within `repl_frame`
673
function CC.builtin_tfunction(interp::REPLInterpreter, @nospecialize(f),
×
674
                              argtypes::Vector{Any}, sv::CC.InferenceState)
675
    if f === Core.getglobal && (interp.limit_aggressive_inference ? is_repl_frame_getproperty(sv) : is_call_graph_uncached(sv))
×
676
        if length(argtypes) == 2
×
677
            a1, a2 = argtypes
×
678
            if isa(a1, Const) && isa(a2, Const)
×
679
                a1val, a2val = a1.val, a2.val
×
680
                if isa(a1val, Module) && isa(a2val, Symbol)
×
681
                    g = GlobalRef(a1val, a2val)
×
682
                    if isdefined_globalref(g)
×
683
                        return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
×
684
                    end
685
                    return Union{}
×
686
                end
687
            end
688
        end
689
    end
690
    return @invoke CC.builtin_tfunction(interp::CC.AbstractInterpreter, f::Any,
×
691
                                        argtypes::Vector{Any}, sv::CC.InferenceState)
692
end
693

694
# aggressive concrete evaluation for `:inconsistent` frames within `repl_frame`
695
function CC.concrete_eval_eligible(interp::REPLInterpreter, @nospecialize(f),
×
696
                                   result::CC.MethodCallResult, arginfo::CC.ArgInfo,
697
                                   sv::CC.InferenceState)
698
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
×
699
        neweffects = CC.Effects(result.effects; consistent=CC.ALWAYS_TRUE)
×
700
        result = CC.MethodCallResult(result.rt, result.exct, neweffects, result.edge,
×
701
                                     result.edgecycle, result.edgelimited, result.volatile_inf_result)
702
    end
703
    ret = @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any,
×
704
                                            result::CC.MethodCallResult, arginfo::CC.ArgInfo,
705
                                            sv::CC.InferenceState)
706
    if ret === :semi_concrete_eval
×
707
        # while the base eligibility check probably won't permit semi-concrete evaluation
708
        # for `REPLInterpreter` (given it completely turns off optimization),
709
        # this ensures we don't inadvertently enter irinterp
710
        ret = :none
×
711
    end
712
    return ret
×
713
end
714

715
# allow constant propagation for mutable constants
716
function CC.const_prop_argument_heuristic(interp::REPLInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
×
717
    if !interp.limit_aggressive_inference
×
718
        any(@nospecialize(a)->isa(a, Const), arginfo.argtypes) && return true # even if mutable
×
719
    end
720
    return @invoke CC.const_prop_argument_heuristic(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
×
721
end
722

723
function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module)
×
724
    @ccall jl_resolve_definition_effects_in_ir(
×
725
        #=jl_array_t *stmts=# src.code::Any,
726
        #=jl_module_t *m=# mod::Any,
727
        #=jl_svec_t *sparam_vals=# Core.svec()::Any,
728
        #=jl_value_t *binding_edge=# C_NULL::Ptr{Cvoid},
729
        #=int binding_effects=# 0::Int)::Cvoid
730
    return src
×
731
end
732

733
# lower `ex` and run type inference on the resulting top-level expression
734
function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressive_inference::Bool=false)
×
735
    if (isexpr(ex, :toplevel) || isexpr(ex, :tuple)) && !isempty(ex.args)
×
736
        # get the inference result for the last expression
737
        ex = ex.args[end]
×
738
    end
739
    lwr = try
×
740
        Meta.lower(context_module, ex)
×
741
    catch # macro expansion failed, etc.
742
        return nothing
×
743
    end
744
    if lwr isa Symbol
×
745
        return isdefined(context_module, lwr) ? Const(getfield(context_module, lwr)) : nothing
×
746
    end
747
    lwr isa Expr || return Const(lwr) # `ex` is literal
×
748
    isexpr(lwr, :thunk) || return nothing # lowered to `Expr(:error, ...)` or similar
×
749
    src = lwr.args[1]::Core.CodeInfo
×
750

751
    resolve_toplevel_symbols!(src, context_module)
×
752
    # construct top-level `MethodInstance`
753
    mi = ccall(:jl_method_instance_for_thunk, Ref{Core.MethodInstance}, (Any, Any), src, context_module)
×
754

755
    interp = REPLInterpreter(limit_aggressive_inference)
×
756
    result = CC.InferenceResult(mi)
×
757
    frame = CC.InferenceState(result, src, #=cache=#:no, interp)
×
758

759
    # NOTE Use the fixed world here to make `REPLInterpreter` robust against
760
    #      potential invalidations of `Core.Compiler` methods.
761
    Base.invoke_in_world(COMPLETION_WORLD[], CC.typeinf, interp, frame)
×
762

763
    result = frame.result.result
×
764
    result === Union{} && return nothing # for whatever reason, callers expect this as the Bottom and/or Top type instead
×
765
    return result
×
766
end
767

768
# `COMPLETION_WORLD[]` will be initialized within `__init__`
769
# (to allow us to potentially remove REPL from the sysimage in the future).
770
# Note that inference from the `code_typed` call below will use the current world age
771
# rather than `typemax(UInt)`, since `Base.invoke_in_world` uses the current world age
772
# when the given world age is higher than the current one.
773
const COMPLETION_WORLD = Ref{UInt}(typemax(UInt))
774

775
# Generate code cache for `REPLInterpreter` now:
776
# This code cache will be available at the world of `COMPLETION_WORLD`,
777
# assuming no invalidation will happen before initializing REPL.
778
# Once REPL is loaded, `REPLInterpreter` will be resilient against future invalidations.
779
code_typed(CC.typeinf, (REPLInterpreter, CC.InferenceState))
780

781
# Method completion on function call expression that look like :(max(1))
782
MAX_METHOD_COMPLETIONS::Int = 40
783
function _complete_methods(ex_org::Expr, context_module::Module, shift::Bool)
×
784
    funct = repl_eval_ex(ex_org.args[1], context_module)
×
785
    funct === nothing && return 2, nothing, [], Set{Symbol}()
×
786
    funct = CC.widenconst(funct)
×
787
    args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex_org, context_module, true, true)
×
788
    return kwargs_flag, funct, args_ex, kwargs_ex
×
789
end
790

791
function complete_methods(ex_org::Expr, context_module::Module=Main, shift::Bool=false)
×
792
    kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex_org, context_module, shift)::Tuple{Int, Any, Vector{Any}, Set{Symbol}}
×
793
    out = Completion[]
×
794
    kwargs_flag == 2 && return out # one of the kwargs is invalid
×
795
    kwargs_flag == 0 && push!(args_ex, Vararg{Any}) # allow more arguments if there is no semicolon
×
796
    complete_methods!(out, funct, args_ex, kwargs_ex, shift ? -2 : MAX_METHOD_COMPLETIONS, kwargs_flag == 1)
×
797
    return out
×
798
end
799

800
MAX_ANY_METHOD_COMPLETIONS::Int = 10
801
function recursive_explore_names!(seen::IdSet, callee_module::Module, initial_module::Module, exploredmodules::IdSet{Module}=IdSet{Module}())
×
802
    push!(exploredmodules, callee_module)
×
803
    for name in names(callee_module; all=true, imported=true)
×
804
        if !Base.isdeprecated(callee_module, name) && !startswith(string(name), '#') && isdefined(initial_module, name)
×
805
            func = getfield(callee_module, name)
×
806
            if !isa(func, Module)
×
807
                funct = Core.Typeof(func)
×
808
                push!(seen, funct)
×
809
            elseif isa(func, Module) && func ∉ exploredmodules
×
810
                recursive_explore_names!(seen, func, initial_module, exploredmodules)
×
811
            end
812
        end
813
    end
×
814
end
815
function recursive_explore_names(callee_module::Module, initial_module::Module)
×
816
    seen = IdSet{Any}()
×
817
    recursive_explore_names!(seen, callee_module, initial_module)
×
818
    seen
×
819
end
820

821
function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool, shift::Bool)
×
822
    out = Completion[]
×
823
    args_ex, kwargs_ex, kwargs_flag = try
×
824
        # this may throw, since we set default_any to false
825
        complete_methods_args(ex_org, context_module, false, false)
×
826
    catch ex
827
        ex isa ArgumentError || rethrow()
×
828
        return out
×
829
    end
830
    kwargs_flag == 2 && return out # one of the kwargs is invalid
×
831

832
    # moreargs determines whether to accept more args, independently of the presence of a
833
    # semicolon for the ".?(" syntax
834
    moreargs && push!(args_ex, Vararg{Any})
×
835

836
    for seen_name in recursive_explore_names(callee_module, callee_module)
×
837
        complete_methods!(out, seen_name, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS, false)
×
838
    end
×
839

840
    if !shift
×
841
        # Filter out methods where all arguments are `Any`
842
        filter!(out) do c
×
843
            isa(c, TextCompletion) && return false
×
844
            isa(c, MethodCompletion) || return true
×
845
            sig = Base.unwrap_unionall(c.method.sig)::DataType
×
846
            return !all(@nospecialize(T) -> T === Any || T === Vararg{Any}, sig.parameters[2:end])
×
847
        end
848
    end
849

850
    return out
×
851
end
852

853
function detect_invalid_kwarg!(kwargs_ex::Vector{Symbol}, @nospecialize(x), kwargs_flag::Int, possible_splat::Bool)
×
854
    n = isexpr(x, :kw) ? x.args[1] : x
×
855
    if n isa Symbol
×
856
        push!(kwargs_ex, n)
×
857
        return kwargs_flag
×
858
    end
859
    possible_splat && isexpr(x, :...) && return kwargs_flag
×
860
    return 2 # The kwarg is invalid
×
861
end
862

863
function detect_args_kwargs(funargs::Vector{Any}, context_module::Module, default_any::Bool, broadcasting::Bool)
×
864
    args_ex = Any[]
×
865
    kwargs_ex = Symbol[]
×
866
    kwargs_flag = 0
×
867
    # kwargs_flag is:
868
    # * 0 if there is no semicolon and no invalid kwarg
869
    # * 1 if there is a semicolon and no invalid kwarg
870
    # * 2 if there are two semicolons or more, or if some kwarg is invalid, which
871
    #        means that it is not of the form "bar=foo", "bar" or "bar..."
872
    for i in (1+!broadcasting):length(funargs)
×
873
        ex = funargs[i]
×
874
        if isexpr(ex, :parameters)
×
875
            kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters
×
876
            for x in ex.args
×
877
                kwargs_flag = detect_invalid_kwarg!(kwargs_ex, x, kwargs_flag, true)
×
878
            end
×
879
        elseif isexpr(ex, :kw)
×
880
            kwargs_flag = detect_invalid_kwarg!(kwargs_ex, ex, kwargs_flag, false)
×
881
        else
882
            if broadcasting
×
883
                # handle broadcasting, but only handle number of arguments instead of
884
                # argument types
885
                push!(args_ex, Any)
×
886
            else
887
                argt = repl_eval_ex(ex, context_module)
×
888
                if argt !== nothing
×
889
                    push!(args_ex, CC.widenconst(argt))
×
890
                elseif default_any
×
891
                    push!(args_ex, Any)
×
892
                else
893
                    throw(ArgumentError("argument not found"))
×
894
                end
895
            end
896
        end
897
    end
×
898
    return args_ex, Set{Symbol}(kwargs_ex), kwargs_flag
×
899
end
900

901
is_broadcasting_expr(ex::Expr) = ex.head === :. && isexpr(ex.args[2], :tuple)
×
902

903
function complete_methods_args(ex::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool)
×
904
    if allow_broadcasting && is_broadcasting_expr(ex)
×
905
        return detect_args_kwargs((ex.args[2]::Expr).args, context_module, default_any, true)
×
906
    end
907
    return detect_args_kwargs(ex.args, context_module, default_any, false)
×
908
end
909

910
function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int, exact_nargs::Bool)
×
911
    # Input types and number of arguments
912
    t_in = Tuple{funct, args_ex...}
×
913
    m = Base._methods_by_ftype(t_in, nothing, max_method_completions, Base.get_world_counter(),
×
914
        #=ambig=# true, Ref(typemin(UInt)), Ref(typemax(UInt)), Ptr{Int32}(C_NULL))
915
    if !isa(m, Vector)
×
916
        push!(out, TextCompletion(sprint(Base.show_signature_function, funct) * "( too many methods, use SHIFT-TAB to show )"))
×
917
        return
×
918
    end
919
    for match in m
×
920
        # TODO: if kwargs_ex, filter out methods without kwargs?
921
        push!(out, MethodCompletion(match.spec_types, match.method))
×
922
    end
×
923
    # TODO: filter out methods with wrong number of arguments if `exact_nargs` is set
924
end
925

926
include("latex_symbols.jl")
927
include("emoji_symbols.jl")
928

929
const non_identifier_chars = [" \t\n\r\"\\'`\$><=:;|&{}()[],+-*/?%^~"...]
930
const whitespace_chars = [" \t\n\r"...]
931
# "\"'`"... is added to whitespace_chars as non of the bslash_completions
932
# characters contain any of these characters. It prohibits the
933
# bslash_completions function to try and complete on escaped characters in strings
934
const bslash_separators = [whitespace_chars..., "\"'`"...]
935

936
const subscripts = Dict(k[3]=>v[1] for (k,v) in latex_symbols if startswith(k, "\\_") && length(k)==3)
937
const subscript_regex = Regex("^\\\\_[" * join(isdigit(k) || isletter(k) ? "$k" : "\\$k" for k in keys(subscripts)) * "]+\\z")
938
const superscripts = Dict(k[3]=>v[1] for (k,v) in latex_symbols if startswith(k, "\\^") && length(k)==3)
939
const superscript_regex = Regex("^\\\\\\^[" * join(isdigit(k) || isletter(k) ? "$k" : "\\$k" for k in keys(superscripts)) * "]+\\z")
940

941
# Aux function to detect whether we're right after a using or import keyword
942
function get_import_mode(s::String)
×
943
    # allow all of these to start with leading whitespace and macros like @eval and @eval(
944
    # ^\s*(?:@\w+\s*(?:\(\s*)?)?
945

946
    # match simple cases like `using |` and `import  |`
947
    mod_import_match_simple = match(r"^\s*(?:@\w+\s*(?:\(\s*)?)?\b(using|import)\s*$", s)
×
948
    if mod_import_match_simple !== nothing
×
949
        if mod_import_match_simple[1] == "using"
×
950
            return :using_module
×
951
        else
952
            return :import_module
×
953
        end
954
    end
955
    # match module import statements like `using Foo|`, `import Foo, Bar|` and `using Foo.Bar, Baz, |`
956
    mod_import_match = match(r"^\s*(?:@\w+\s*(?:\(\s*)?)?\b(using|import)\s+([\w\.]+(?:\s*,\s*[\w\.]+)*),?\s*$", s)
×
957
    if mod_import_match !== nothing
×
958
        if mod_import_match.captures[1] == "using"
×
959
            return :using_module
×
960
        else
961
            return :import_module
×
962
        end
963
    end
964
    # now match explicit name import statements like `using Foo: |` and `import Foo: bar, baz|`
965
    name_import_match = match(r"^\s*(?:@\w+\s*(?:\(\s*)?)?\b(using|import)\s+([\w\.]+)\s*:\s*([\w@!\s,]+)$", s)
×
966
    if name_import_match !== nothing
×
967
        if name_import_match[1] == "using"
×
968
            return :using_name
×
969
        else
970
            return :import_name
×
971
        end
972
    end
973
    return nothing
×
974
end
975

976
function close_path_completion(dir, path, str, pos)
×
977
    path = unescape_string(replace(path, "\\\$"=>"\$"))
×
978
    path = joinpath(dir, path)
×
979
    # ...except if it's a directory...
980
    Base.isaccessibledir(path) && return false
×
981
    # ...and except if there's already a " at the cursor.
982
    return lastindex(str) <= pos || str[nextind(str, pos)] != '"'
×
983
end
984

985
function bslash_completions(string::String, pos::Int, hint::Bool=false)
×
986
    slashpos = something(findprev(isequal('\\'), string, pos), 0)
×
987
    if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos &&
×
988
        !(1 < slashpos && (string[prevind(string, slashpos)]=='\\')))
989
        # latex / emoji symbol substitution
990
        s = string[slashpos:pos]
×
991
        latex = get(latex_symbols, s, "")
×
992
        if !isempty(latex) # complete an exact match
×
993
            return (true, (Completion[BslashCompletion(latex)], slashpos:pos, true))
×
994
        elseif occursin(subscript_regex, s)
×
995
            sub = map(c -> subscripts[c], s[3:end])
×
996
            return (true, (Completion[BslashCompletion(sub)], slashpos:pos, true))
×
997
        elseif occursin(superscript_regex, s)
×
998
            sup = map(c -> superscripts[c], s[3:end])
×
999
            return (true, (Completion[BslashCompletion(sup)], slashpos:pos, true))
×
1000
        end
1001
        emoji = get(emoji_symbols, s, "")
×
1002
        if !isempty(emoji)
×
1003
            return (true, (Completion[BslashCompletion(emoji)], slashpos:pos, true))
×
1004
        end
1005
        # return possible matches; these cannot be mixed with regular
1006
        # Julian completions as only latex / emoji symbols contain the leading \
1007
        symbol_dict = startswith(s, "\\:") ? emoji_symbols : latex_symbols
×
1008
        namelist = Iterators.filter(k -> startswith(k, s), keys(symbol_dict))
×
1009
        completions = Completion[BslashCompletion(name, "$(symbol_dict[name]) $name") for name in sort!(collect(namelist))]
×
1010
        return (true, (completions, slashpos:pos, true))
×
1011
    end
1012
    return (false, (Completion[], 0:-1, false))
×
1013
end
1014

1015
function dict_identifier_key(str::String, tag::Symbol, context_module::Module=Main)
×
1016
    if tag === :string
×
1017
        str_close = str*"\""
×
1018
    elseif tag === :cmd
×
1019
        str_close = str*"`"
×
1020
    else
1021
        str_close = str
×
1022
    end
1023
    frange, end_of_identifier = find_start_brace(str_close, c_start='[', c_end=']')
×
1024
    isempty(frange) && return (nothing, nothing, nothing)
×
1025
    objstr = str[1:end_of_identifier]
×
1026
    objex = Meta.parse(objstr, raise=false, depwarn=false)
×
1027
    objt = repl_eval_ex(objex, context_module)
×
1028
    isa(objt, Core.Const) || return (nothing, nothing, nothing)
×
1029
    obj = objt.val
×
1030
    isa(obj, AbstractDict) || return (nothing, nothing, nothing)
×
1031
    (Base.haslength(obj) && length(obj)::Int < 1_000_000) || return (nothing, nothing, nothing)
×
1032
    begin_of_key = something(findnext(!isspace, str, nextind(str, end_of_identifier) + 1), # +1 for [
×
1033
                             lastindex(str)+1)
1034
    return (obj, str[begin_of_key:end], begin_of_key)
×
1035
end
1036

1037
# This needs to be a separate non-inlined function, see #19441
1038
@noinline function find_dict_matches(identifier::AbstractDict, partial_key)
×
1039
    matches = String[]
×
1040
    for key in keys(identifier)
×
1041
        rkey = repr(key)
×
1042
        startswith(rkey,partial_key) && push!(matches,rkey)
×
1043
    end
×
1044
    return matches
×
1045
end
1046

1047
# Identify an argument being completed in a method call. If the argument is empty, method
1048
# suggestions will be provided instead of argument completions.
1049
function identify_possible_method_completion(partial, last_idx)
×
1050
    fail = 0:-1, Expr(:nothing), 0:-1, 0
×
1051

1052
    # First, check that the last punctuation is either ',', ';' or '('
1053
    idx_last_punct = something(findprev(x -> ispunct(x) && x != '_' && x != '!', partial, last_idx), 0)::Int
×
1054
    idx_last_punct == 0 && return fail
×
1055
    last_punct = partial[idx_last_punct]
×
1056
    last_punct == ',' || last_punct == ';' || last_punct == '(' || return fail
×
1057

1058
    # Then, check that `last_punct` is only followed by an identifier or nothing
1059
    before_last_word_start = something(findprev(in(non_identifier_chars), partial, last_idx), 0)
×
1060
    before_last_word_start == 0 && return fail
×
1061
    all(isspace, @view partial[nextind(partial, idx_last_punct):before_last_word_start]) || return fail
×
1062

1063
    # Check that `last_punct` is either the last '(' or placed after a previous '('
1064
    frange, method_name_end = find_start_brace(@view partial[1:idx_last_punct])
×
1065
    method_name_end ∈ frange || return fail
×
1066

1067
    # Strip the preceding ! operators, if any, and close the expression with a ')'
1068
    s = replace(partial[frange], r"\G\!+([^=\(]+)" => s"\1"; count=1) * ')'
×
1069
    ex = Meta.parse(s, raise=false, depwarn=false)
×
1070
    isa(ex, Expr) || return fail
×
1071

1072
    # `wordrange` is the position of the last argument to complete
1073
    wordrange = nextind(partial, before_last_word_start):last_idx
×
1074
    return frange, ex, wordrange, method_name_end
×
1075
end
1076

1077
# Provide completion for keyword arguments in function calls
1078
function complete_keyword_argument(partial::String, last_idx::Int, context_module::Module;
×
1079
                                   shift::Bool=false)
1080
    frange, ex, wordrange, = identify_possible_method_completion(partial, last_idx)
×
1081
    fail = Completion[], 0:-1, frange
×
1082
    ex.head === :call || is_broadcasting_expr(ex) || return fail
×
1083

1084
    kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex, context_module, true)::Tuple{Int, Any, Vector{Any}, Set{Symbol}}
×
1085
    kwargs_flag == 2 && return fail # one of the previous kwargs is invalid
×
1086

1087
    methods = Completion[]
×
1088
    complete_methods!(methods, funct, Any[Vararg{Any}], kwargs_ex, shift ? -1 : MAX_METHOD_COMPLETIONS, kwargs_flag == 1)
×
1089
    # TODO: use args_ex instead of Any[Vararg{Any}] and only provide kwarg completion for
1090
    # method calls compatible with the current arguments.
1091

1092
    # For each method corresponding to the function call, provide completion suggestions
1093
    # for each keyword that starts like the last word and that is not already used
1094
    # previously in the expression. The corresponding suggestion is "kwname=".
1095
    # If the keyword corresponds to an existing name, also include "kwname" as a suggestion
1096
    # since the syntax "foo(; kwname)" is equivalent to "foo(; kwname=kwname)".
1097
    last_word = partial[wordrange] # the word to complete
×
1098
    kwargs = Set{String}()
×
1099
    for m in methods
×
1100
        # if MAX_METHOD_COMPLETIONS is hit a single TextCompletion is return by complete_methods! with an explanation
1101
        # which can be ignored here
1102
        m isa TextCompletion && continue
×
1103
        m::MethodCompletion
×
1104
        possible_kwargs = Base.kwarg_decl(m.method)
×
1105
        current_kwarg_candidates = String[]
×
1106
        for _kw in possible_kwargs
×
1107
            kw = String(_kw)
×
1108
            if !endswith(kw, "...") && startswith(kw, last_word) && _kw ∉ kwargs_ex
×
1109
                push!(current_kwarg_candidates, kw)
×
1110
            end
1111
        end
×
1112
        union!(kwargs, current_kwarg_candidates)
×
1113
    end
×
1114

1115
    suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs]
×
1116

1117
    # Only add these if not in kwarg space. i.e. not in `foo(; `
1118
    if kwargs_flag == 0
×
1119
        complete_symbol!(suggestions, #=prefix=#nothing, last_word, context_module; shift)
×
1120
        complete_keyval!(suggestions, last_word)
×
1121
    end
1122

1123
    return sort!(suggestions, by=named_completion_completion), wordrange
×
1124
end
1125

1126
function get_loading_candidates(pkgstarts::String, project_file::String)
×
1127
    loading_candidates = String[]
×
1128
    d = Base.parsed_toml(project_file)
×
1129
    pkg = get(d, "name", nothing)::Union{String, Nothing}
×
1130
    if pkg !== nothing && startswith(pkg, pkgstarts)
×
1131
        push!(loading_candidates, pkg)
×
1132
    end
1133
    deps = get(d, "deps", nothing)::Union{Dict{String, Any}, Nothing}
×
1134
    if deps !== nothing
×
1135
        for (pkg, _) in deps
×
1136
            startswith(pkg, pkgstarts) && push!(loading_candidates, pkg)
×
1137
        end
×
1138
    end
1139
    return loading_candidates
×
1140
end
1141

1142
function complete_loading_candidates!(suggestions::Vector{Completion}, pkgstarts::String, project_file::String)
×
1143
    for name in get_loading_candidates(pkgstarts, project_file)
×
1144
        push!(suggestions, PackageCompletion(name))
×
1145
    end
×
1146
    return suggestions
×
1147
end
1148

1149
function complete_identifiers!(suggestions::Vector{Completion},
×
1150
                               context_module::Module, string::String, name::String,
1151
                               pos::Int, separatorpos::Int, startpos::Int;
1152
                               comp_keywords::Bool=false,
1153
                               complete_modules_only::Bool=false,
1154
                               shift::Bool=false)
1155
    if comp_keywords
×
1156
        complete_keyword!(suggestions, name)
×
1157
        complete_keyval!(suggestions, name)
×
1158
    end
1159
    if separatorpos > 1 && (string[separatorpos] == '.' || string[separatorpos] == ':')
×
1160
        s = string[1:prevind(string, separatorpos)]
×
1161
        # First see if the whole string up to `pos` is a valid expression. If so, use it.
1162
        prefix = Meta.parse(s, raise=false, depwarn=false)
×
1163
        if isexpr(prefix, :incomplete)
×
1164
            s = string[startpos:pos]
×
1165
            # Heuristic to find the start of the expression. TODO: This would be better
1166
            # done with a proper error-recovering parser.
1167
            if 0 < startpos <= lastindex(string) && string[startpos] == '.'
×
1168
                i = prevind(string, startpos)
×
1169
                while 0 < i
×
1170
                    c = string[i]
×
1171
                    if c in (')', ']')
×
1172
                        if c == ')'
×
1173
                            c_start = '('
×
1174
                            c_end = ')'
×
1175
                        elseif c == ']'
×
1176
                            c_start = '['
×
1177
                            c_end = ']'
×
1178
                        end
1179
                        frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
×
1180
                        isempty(frange) && break # unbalanced parens
×
1181
                        startpos = first(frange)
×
1182
                        i = prevind(string, startpos)
×
1183
                    elseif c in ('\'', '\"', '\`')
×
1184
                        s = "$c$c"*string[startpos:pos]
×
1185
                        break
×
1186
                    else
1187
                        break
×
1188
                    end
1189
                    s = string[startpos:pos]
×
1190
                end
×
1191
            end
1192
            if something(findlast(in(non_identifier_chars), s), 0) < something(findlast(isequal('.'), s), 0)
×
1193
                lookup_name, name = rsplit(s, ".", limit=2)
×
1194
                name = String(name)
×
1195
                prefix = Meta.parse(lookup_name, raise=false, depwarn=false)
×
1196
            end
1197
            isexpr(prefix, :incomplete) && (prefix = nothing)
×
1198
        elseif isexpr(prefix, (:using, :import))
×
1199
            arglast = prefix.args[end] # focus on completion to the last argument
×
1200
            if isexpr(arglast, :.)
×
1201
                # We come here for cases like:
1202
                # - `string`: "using Mod1.Mod2.M"
1203
                # - `ex`: :(using Mod1.Mod2)
1204
                # - `name`: "M"
1205
                # Now we transform `ex` to `:(Mod1.Mod2)` to allow `complete_symbol!` to
1206
                # complete for inner modules whose name starts with `M`.
1207
                # Note that `complete_modules_only=true` is set within `completions`
1208
                prefix = nothing
×
1209
                firstdot = true
×
1210
                for arg = arglast.args
×
1211
                    if arg === :.
×
1212
                        # override `context_module` if multiple `.` accessors are used
1213
                        if firstdot
×
1214
                            firstdot = false
×
1215
                        else
1216
                            context_module = parentmodule(context_module)
×
1217
                        end
1218
                    elseif arg isa Symbol
×
1219
                        if prefix === nothing
×
1220
                            prefix = arg
×
1221
                        else
1222
                            prefix = Expr(:., prefix, QuoteNode(arg))
×
1223
                        end
1224
                    else # invalid expression
1225
                        prefix = nothing
×
1226
                        break
×
1227
                    end
1228
                end
×
1229
            end
1230
        elseif isexpr(prefix, :call) && length(prefix.args) > 1
×
1231
            isinfix = s[end] != ')'
×
1232
            # A complete call expression that does not finish with ')' is an infix call.
1233
            if !isinfix
×
1234
                # Handle infix call argument completion of the form bar + foo(qux).
1235
                frange, end_of_identifier = find_start_brace(@view s[1:prevind(s, end)])
×
1236
                if !isempty(frange) # if find_start_brace fails to find the brace just continue
×
1237
                    isinfix = Meta.parse(@view(s[frange[1]:end]), raise=false, depwarn=false) == prefix.args[end]
×
1238
                end
1239
            end
1240
            if isinfix
×
1241
                prefix = prefix.args[end]
×
1242
            end
1243
        elseif isexpr(prefix, :macrocall) && length(prefix.args) > 1
×
1244
            # allow symbol completions within potentially incomplete macrocalls
1245
            if s[end] ≠ '`' && s[end] ≠ ')'
×
1246
                prefix = prefix.args[end]
×
1247
            end
1248
        end
1249
    else
1250
        prefix = nothing
×
1251
    end
1252
    complete_symbol!(suggestions, prefix, name, context_module; complete_modules_only, shift)
×
1253
    return suggestions
×
1254
end
1255

1256
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false)
×
1257
    # First parse everything up to the current position
1258
    partial = string[1:pos]
×
1259
    inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false))
×
1260

1261
    if !hint # require a tab press for completion of these
×
1262
        # ?(x, y)TAB lists methods you can call with these objects
1263
        # ?(x, y TAB lists methods that take these objects as the first two arguments
1264
        # MyModule.?(x, y)TAB restricts the search to names in MyModule
1265
        rexm = match(r"(\w+\.|)\?\((.*)$", partial)
×
1266
        if rexm !== nothing
×
1267
            # Get the module scope
1268
            if isempty(rexm.captures[1])
×
1269
                callee_module = context_module
×
1270
            else
1271
                modname = Symbol(rexm.captures[1][1:end-1])
×
1272
                if isdefined(context_module, modname)
×
1273
                    callee_module = getfield(context_module, modname)
×
1274
                    if !isa(callee_module, Module)
×
1275
                        callee_module = context_module
×
1276
                    end
1277
                else
1278
                    callee_module = context_module
×
1279
                end
1280
            end
1281
            moreargs = !endswith(rexm.captures[2], ')')
×
1282
            callstr = "_(" * rexm.captures[2]
×
1283
            if moreargs
×
1284
                callstr *= ')'
×
1285
            end
1286
            ex_org = Meta.parse(callstr, raise=false, depwarn=false)
×
1287
            if isa(ex_org, Expr)
×
1288
                return complete_any_methods(ex_org, callee_module::Module, context_module, moreargs, shift), (0:length(rexm.captures[1])+1) .+ rexm.offset, false
×
1289
            end
1290
        end
1291
    end
1292

1293
    # if completing a key in a Dict
1294
    identifier, partial_key, loc = dict_identifier_key(partial, inc_tag, context_module)
×
1295
    if identifier !== nothing
×
1296
        matches = find_dict_matches(identifier, partial_key)
×
1297
        length(matches)==1 && (lastindex(string) <= pos || string[nextind(string,pos)] != ']') && (matches[1]*=']')
×
1298
        length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true
×
1299
    end
1300

1301
    suggestions = Completion[]
×
1302

1303
    # Check if this is a var"" string macro that should be completed like
1304
    # an identifier rather than a string.
1305
    # TODO: It would be nice for the parser to give us more information here
1306
    # so that we can lookup the macro by identity rather than pattern matching
1307
    # its invocation.
1308
    varrange = findprev("var\"", string, pos)
×
1309

1310
    expanded = nothing
×
1311
    was_expanded = false
×
1312

1313
    if varrange !== nothing
×
1314
        ok, ret = bslash_completions(string, pos)
×
1315
        ok && return ret
×
1316
        startpos = first(varrange) + 4
×
1317
        separatorpos = something(findprev(isequal('.'), string, first(varrange)-1), 0)
×
1318
        name = string[startpos:pos]
×
1319
        complete_identifiers!(suggestions, context_module, string, name,
×
1320
                              pos, separatorpos, startpos;
1321
                              shift)
1322
        return sort!(unique!(named_completion, suggestions), by=named_completion_completion), (separatorpos+1):pos, true
×
1323
    elseif inc_tag === :cmd
×
1324
        # TODO: should this call shell_completions instead of partially reimplementing it?
1325
        let m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial)) # fuzzy shell_parse in reverse
×
1326
            startpos = nextind(partial, reverseind(partial, m.offset))
×
1327
            r = startpos:pos
×
1328
            scs::String = string[r]
×
1329

1330
            expanded = complete_expanduser(scs, r)
×
1331
            was_expanded = expanded[3]
×
1332
            if was_expanded
×
1333
                scs = (only(expanded[1])::PathCompletion).path
×
1334
                # If tab press, ispath and user expansion available, return it now
1335
                # otherwise see if we can complete the path further before returning with expanded ~
1336
                !hint && ispath(scs) && return expanded::Completions
×
1337
            end
1338

1339
            path::String = replace(scs, r"(\\+)\g1(\\?)`" => "\1\2`") # fuzzy unescape_raw_string: match an even number of \ before ` and replace with half as many
×
1340
            # This expansion with "\\ "=>' ' replacement and shell_escape=true
1341
            # assumes the path isn't further quoted within the cmd backticks.
1342
            path = replace(path, r"\\ " => " ", r"\$" => "\$") # fuzzy shell_parse (reversed by shell_escape_posixly)
×
1343
            paths, dir, success = complete_path(path, shell_escape=true, raw_escape=true)
×
1344

1345
            if success && !isempty(dir)
×
1346
                let dir = do_raw_escape(do_shell_escape(dir))
×
1347
                    # if escaping of dir matches scs prefix, remove that from the completions
1348
                    # otherwise make it the whole completion
1349
                    if endswith(dir, "/") && startswith(scs, dir)
×
1350
                        r = (startpos + sizeof(dir)):pos
×
1351
                    elseif startswith(scs, dir * "/")
×
1352
                        r = nextind(string, startpos + sizeof(dir)):pos
×
1353
                    else
1354
                        map!(paths, paths) do c::PathCompletion
×
1355
                            p = dir * "/" * c.path
×
1356
                            was_expanded && (p = contractuser(p))
×
1357
                            return PathCompletion(p)
×
1358
                        end
1359
                    end
1360
                end
1361
            end
1362
            if isempty(paths) && !hint && was_expanded
×
1363
                # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
1364
                return expanded::Completions
×
1365
            else
1366
                return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
×
1367
            end
1368
        end
1369
    elseif inc_tag === :string
×
1370
        # Find first non-escaped quote
1371
        let m = match(r"\"(?!\\)", reverse(partial))
×
1372
            startpos = nextind(partial, reverseind(partial, m.offset))
×
1373
            r = startpos:pos
×
1374
            scs::String = string[r]
×
1375

1376
            expanded = complete_expanduser(scs, r)
×
1377
            was_expanded = expanded[3]
×
1378
            if was_expanded
×
1379
                scs = (only(expanded[1])::PathCompletion).path
×
1380
                # If tab press, ispath and user expansion available, return it now
1381
                # otherwise see if we can complete the path further before returning with expanded ~
1382
                !hint && ispath(scs) && return expanded::Completions
×
1383
            end
1384

1385
            path = try
×
1386
                unescape_string(replace(scs, "\\\$"=>"\$"))
×
1387
            catch ex
1388
                ex isa ArgumentError || rethrow()
×
1389
                nothing
×
1390
            end
1391
            if !isnothing(path)
×
1392
                paths, dir, success = complete_path(path::String, string_escape=true)
×
1393

1394
                if length(paths) == 1
×
1395
                    p = (paths[1]::PathCompletion).path
×
1396
                    hint && was_expanded && (p = contractuser(p))
×
1397
                    if close_path_completion(dir, p, path, pos)
×
1398
                        paths[1] = PathCompletion(p * "\"")
×
1399
                    end
1400
                end
1401

1402
                if success && !isempty(dir)
×
1403
                    let dir = do_string_escape(dir)
×
1404
                        # if escaping of dir matches scs prefix, remove that from the completions
1405
                        # otherwise make it the whole completion
1406
                        if endswith(dir, "/") && startswith(scs, dir)
×
1407
                            r = (startpos + sizeof(dir)):pos
×
1408
                        elseif startswith(scs, dir * "/") && dir != dirname(homedir())
×
1409
                            was_expanded && (dir = contractuser(dir))
×
1410
                            r = nextind(string, startpos + sizeof(dir)):pos
×
1411
                        else
1412
                            map!(paths, paths) do c::PathCompletion
×
1413
                                p = dir * "/" * c.path
×
1414
                                hint && was_expanded && (p = contractuser(p))
×
1415
                                return PathCompletion(p)
×
1416
                            end
1417
                        end
1418
                    end
1419
                end
1420

1421
                # Fallthrough allowed so that Latex symbols can be completed in strings
1422
                if success
×
1423
                    return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
×
1424
                elseif !hint && was_expanded
×
1425
                    # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
1426
                    return expanded::Completions
×
1427
                end
1428
            end
1429
        end
1430
    end
1431
    # if path has ~ and we didn't find any paths to complete just return the expanded path
1432
    was_expanded && return expanded::Completions
×
1433

1434
    ok, ret = bslash_completions(string, pos)
×
1435
    ok && return ret
×
1436

1437
    # Make sure that only bslash_completions is working on strings
1438
    inc_tag === :string && return Completion[], 0:-1, false
×
1439
    if inc_tag === :other
×
1440
        frange, ex, wordrange, method_name_end = identify_possible_method_completion(partial, pos)
×
1441
        if last(frange) != -1 && all(isspace, @view partial[wordrange]) # no last argument to complete
×
1442
            if ex.head === :call
×
1443
                return complete_methods(ex, context_module, shift), first(frange):method_name_end, false
×
1444
            elseif is_broadcasting_expr(ex)
×
1445
                return complete_methods(ex, context_module, shift), first(frange):(method_name_end - 1), false
×
1446
            end
1447
        end
1448
    elseif inc_tag === :comment
×
1449
        return Completion[], 0:-1, false
×
1450
    end
1451

1452
    # Check whether we can complete a keyword argument in a function call
1453
    kwarg_completion, wordrange = complete_keyword_argument(partial, pos, context_module; shift)
×
1454
    isempty(wordrange) || return kwarg_completion, wordrange, !isempty(kwarg_completion)
×
1455

1456
    startpos = nextind(string, something(findprev(in(non_identifier_chars), string, pos), 0))
×
1457
    # strip preceding ! operator
1458
    if (m = match(r"\G\!+", partial, startpos)) isa RegexMatch
×
1459
        startpos += length(m.match)
×
1460
    end
1461

1462
    separatorpos = something(findprev(isequal('.'), string, pos), 0)
×
1463
    namepos = max(startpos, separatorpos+1)
×
1464
    name = string[namepos:pos]
×
1465
    import_mode = get_import_mode(string)
×
1466
    if import_mode === :using_module || import_mode === :import_module
×
1467
        # Given input lines like `using Foo|`, `import Foo, Bar|` and `using Foo.Bar, Baz, |`:
1468
        # Let's look only for packages and modules we can reach from here
1469

1470
        # If there's no dot, we're in toplevel, so we should
1471
        # also search for packages
1472
        s = string[startpos:pos]
×
1473
        if separatorpos <= startpos
×
1474
            for dir in Base.load_path()
×
1475
                if basename(dir) in Base.project_names && isfile(dir)
×
1476
                    complete_loading_candidates!(suggestions, s, dir)
×
1477
                end
1478
                isdir(dir) || continue
×
1479
                for entry in _readdirx(dir)
×
1480
                    pname = entry.name
×
1481
                    if pname[1] != '.' && pname != "METADATA" &&
×
1482
                        pname != "REQUIRE" && startswith(pname, s)
1483
                        # Valid file paths are
1484
                        #   <Mod>.jl
1485
                        #   <Mod>/src/<Mod>.jl
1486
                        #   <Mod>.jl/src/<Mod>.jl
1487
                        if isfile(entry)
×
1488
                            endswith(pname, ".jl") && push!(suggestions,
×
1489
                                                            PackageCompletion(pname[1:prevind(pname, end-2)]))
1490
                        else
1491
                            mod_name = if endswith(pname, ".jl")
×
1492
                                pname[1:prevind(pname, end-2)]
×
1493
                            else
1494
                                pname
×
1495
                            end
1496
                            if isfile(joinpath(entry, "src",
×
1497
                                               "$mod_name.jl"))
1498
                                push!(suggestions, PackageCompletion(mod_name))
×
1499
                            end
1500
                        end
1501
                    end
1502
                end
×
1503
            end
×
1504
        end
1505
        comp_keywords = false
×
1506
        complete_modules_only = import_mode === :using_module # allow completion for `import Mod.name` (where `name` is not a module)
×
1507
    elseif import_mode === :using_name || import_mode === :import_name
×
1508
        # `using Foo: |` and `import Foo: bar, baz|`
1509
        separatorpos = findprev(isequal(':'), string, pos)::Int
×
1510
        comp_keywords = false
×
1511
        complete_modules_only = false
×
1512
    else
1513
        comp_keywords = !isempty(name) && startpos > separatorpos
×
1514
        complete_modules_only = false
×
1515
    end
1516

1517
    complete_identifiers!(suggestions, context_module, string, name,
×
1518
                          pos, separatorpos, startpos;
1519
                          comp_keywords, complete_modules_only, shift)
1520
    return sort!(unique!(named_completion, suggestions), by=named_completion_completion), namepos:pos, true
×
1521
end
1522

1523
function shell_completions(string, pos, hint::Bool=false)
×
1524
    # First parse everything up to the current position
1525
    scs = string[1:pos]
×
1526
    args, last_arg_start = try
×
1527
        Base.shell_parse(scs, true)::Tuple{Expr,Int}
×
1528
    catch ex
1529
        ex isa ArgumentError || ex isa ErrorException || rethrow()
×
1530
        return Completion[], 0:-1, false
×
1531
    end
1532
    ex = args.args[end]::Expr
×
1533
    # Now look at the last thing we parsed
1534
    isempty(ex.args) && return Completion[], 0:-1, false
×
1535
    lastarg = ex.args[end]
×
1536
    # As Base.shell_parse throws away trailing spaces (unless they are escaped),
1537
    # we need to special case here.
1538
    # If the last char was a space, but shell_parse ignored it search on "".
1539
    if isexpr(lastarg, :incomplete) || isexpr(lastarg, :error)
×
1540
        partial = string[last_arg_start:pos]
×
1541
        ret, range = completions(partial, lastindex(partial), Main, true, hint)
×
1542
        range = range .+ (last_arg_start - 1)
×
1543
        return ret, range, true
×
1544
    elseif endswith(scs, ' ') && !endswith(scs, "\\ ")
×
1545
        r = pos+1:pos
×
1546
        paths, dir, success = complete_path("", use_envpath=false, shell_escape=true)
×
1547
        return paths, r, success
×
1548
    elseif all(@nospecialize(arg) -> arg isa AbstractString, ex.args)
×
1549
        # Join these and treat this as a path
1550
        path::String = join(ex.args)
×
1551
        r = last_arg_start:pos
×
1552

1553
        # Also try looking into the env path if the user wants to complete the first argument
1554
        use_envpath = length(args.args) < 2
×
1555

1556
        expanded = complete_expanduser(path, r)
×
1557
        was_expanded = expanded[3]
×
1558
        if was_expanded
×
1559
            path = (only(expanded[1])::PathCompletion).path
×
1560
            # If tab press, ispath and user expansion available, return it now
1561
            # otherwise see if we can complete the path further before returning with expanded ~
1562
            !hint && ispath(path) && return expanded::Completions
×
1563
        end
1564

1565
        paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true, contract_user=was_expanded)
×
1566

1567
        if success && !isempty(dir)
×
1568
            let dir = do_shell_escape(dir)
×
1569
                # if escaping of dir matches scs prefix, remove that from the completions
1570
                # otherwise make it the whole completion
1571
                partial = string[last_arg_start:pos]
×
1572
                if endswith(dir, "/") && startswith(partial, dir)
×
1573
                    r = (last_arg_start + sizeof(dir)):pos
×
1574
                elseif startswith(partial, dir * "/")
×
1575
                    r = nextind(string, last_arg_start + sizeof(dir)):pos
×
1576
                else
1577
                    map!(paths, paths) do c::PathCompletion
×
1578
                        return PathCompletion(dir * "/" * c.path)
×
1579
                    end
1580
                end
1581
            end
1582
        end
1583
        # if ~ was expanded earlier and the incomplete string isn't a path
1584
        # return the path with contracted user to match what the hint shows. Otherwise expand ~
1585
        # i.e. require two tab presses to expand user
1586
        if was_expanded && !ispath(path)
×
1587
            map!(paths, paths) do c::PathCompletion
×
1588
                PathCompletion(contractuser(c.path))
×
1589
            end
1590
        end
1591
        return paths, r, success
×
1592
    end
1593
    return Completion[], 0:-1, false
×
1594
end
1595

1596
function __init__()
1✔
1597
    COMPLETION_WORLD[] = Base.get_world_counter()
1✔
1598
    return nothing
1✔
1599
end
1600

1601
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