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

JuliaLang / julia / #37836

11 Jul 2024 11:31AM UTC coverage: 85.36% (-2.2%) from 87.516%
#37836

push

local

web-flow
fix loading of repeated/concurrent modules (#55066)

More followup to fix issues with require. There was an accidental
variable reuse (build_id) that caused it to be unable to load cache
files in many cases. There was also missing check for a dependency
already being loaded, resulting in trying to load it twice. Finally, the
start_loading code may drop the require_lock, but the surrounding code
was not prepared for that. Now integrate the necessary checks into
start_loading, instead of needing to duplicate them before and
afterwards.

Fixes #53983
Fixes #54940
Closes #55064

74 of 89 new or added lines in 1 file covered. (83.15%)

2133 existing lines in 46 files now uncovered.

73916 of 86593 relevant lines covered (85.36%)

15331627.57 hits per line

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

8.47
/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
6

7
using Core: Const
8
const CC = Core.Compiler
9
using Base.Meta
10
using Base: propertynames, something, IdSet
11
using Base.Filesystem: _readdirx
12

13
abstract type Completion end
14

15
struct TextCompletion <: Completion
UNCOV
16
    text::String
×
17
end
18

19
struct KeywordCompletion <: Completion
20
    keyword::String
21
end
22

23
struct KeyvalCompletion <: Completion
24
    keyval::String
25
end
26

27
struct PathCompletion <: Completion
28
    path::String
29
end
30

31
struct ModuleCompletion <: Completion
32
    parent::Module
33
    mod::String
34
end
35

36
struct PackageCompletion <: Completion
37
    package::String
38
end
39

40
struct PropertyCompletion <: Completion
41
    value
42
    property::Symbol
43
end
44

45
struct FieldCompletion <: Completion
46
    typ::DataType
47
    field::Symbol
48
end
49

50
struct MethodCompletion <: Completion
51
    tt # may be used by an external consumer to infer return type, etc.
52
    method::Method
53
    MethodCompletion(@nospecialize(tt), method::Method) = new(tt, method)
1✔
54
end
55

56
struct BslashCompletion <: Completion
57
    bslash::String
58
end
59

60
struct ShellCompletion <: Completion
61
    text::String
62
end
63

64
struct DictCompletion <: Completion
65
    dict::AbstractDict
66
    key::String
67
end
68

69
struct KeywordArgumentCompletion <: Completion
70
    kwarg::String
71
end
72

73
# interface definition
74
function Base.getproperty(c::Completion, name::Symbol)
75
    if name === :text
1✔
UNCOV
76
        return getfield(c, :text)::String
×
77
    elseif name === :keyword
1✔
UNCOV
78
        return getfield(c, :keyword)::String
×
79
    elseif name === :path
1✔
UNCOV
80
        return getfield(c, :path)::String
×
81
    elseif name === :parent
1✔
82
        return getfield(c, :parent)::Module
×
83
    elseif name === :mod
1✔
UNCOV
84
        return getfield(c, :mod)::String
×
85
    elseif name === :package
1✔
UNCOV
86
        return getfield(c, :package)::String
×
87
    elseif name === :property
1✔
UNCOV
88
        return getfield(c, :property)::Symbol
×
89
    elseif name === :field
1✔
UNCOV
90
        return getfield(c, :field)::Symbol
×
UNCOV
91
    elseif name === :method
×
92
        return getfield(c, :method)::Method
1✔
UNCOV
93
    elseif name === :bslash
×
UNCOV
94
        return getfield(c, :bslash)::String
×
UNCOV
95
    elseif name === :text
×
96
        return getfield(c, :text)::String
×
UNCOV
97
    elseif name === :key
×
UNCOV
98
        return getfield(c, :key)::String
×
UNCOV
99
    elseif name === :kwarg
×
UNCOV
100
        return getfield(c, :kwarg)::String
×
101
    end
UNCOV
102
    return getfield(c, name)
×
103
end
104

UNCOV
105
_completion_text(c::TextCompletion) = c.text
×
UNCOV
106
_completion_text(c::KeywordCompletion) = c.keyword
×
UNCOV
107
_completion_text(c::KeyvalCompletion) = c.keyval
×
UNCOV
108
_completion_text(c::PathCompletion) = c.path
×
UNCOV
109
_completion_text(c::ModuleCompletion) = c.mod
×
UNCOV
110
_completion_text(c::PackageCompletion) = c.package
×
UNCOV
111
_completion_text(c::PropertyCompletion) = sprint(Base.show_sym, c.property)
×
UNCOV
112
_completion_text(c::FieldCompletion) = sprint(Base.show_sym, c.field)
×
113
_completion_text(c::MethodCompletion) = repr(c.method)
1✔
UNCOV
114
_completion_text(c::BslashCompletion) = c.bslash
×
115
_completion_text(c::ShellCompletion) = c.text
×
UNCOV
116
_completion_text(c::DictCompletion) = c.key
×
UNCOV
117
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='
×
118

119
completion_text(c) = _completion_text(c)::String
1✔
120

121
const Completions = Tuple{Vector{Completion}, UnitRange{Int}, Bool}
122

UNCOV
123
function completes_global(x, name)
×
UNCOV
124
    return startswith(x, name) && !('#' in x)
×
125
end
126

UNCOV
127
function appendmacro!(syms, macros, needle, endchar)
×
UNCOV
128
    for macsym in macros
×
UNCOV
129
        s = String(macsym)
×
UNCOV
130
        if endswith(s, needle)
×
UNCOV
131
            from = nextind(s, firstindex(s))
×
UNCOV
132
            to = prevind(s, sizeof(s)-sizeof(needle)+1)
×
UNCOV
133
            push!(syms, s[from:to]*endchar)
×
134
        end
UNCOV
135
    end
×
136
end
137

UNCOV
138
function append_filtered_mod_names!(ffunc::Function, suggestions::Vector{Completion},
×
139
                                    mod::Module, name::String, complete_internal_only::Bool)
UNCOV
140
    imported = usings = !complete_internal_only
×
UNCOV
141
    ssyms = names(mod; all=true, imported, usings)
×
UNCOV
142
    filter!(ffunc, ssyms)
×
UNCOV
143
    macros = filter(x -> startswith(String(x), "@" * name), ssyms)
×
UNCOV
144
    syms = String[sprint((io,s)->Base.show_sym(io, s; allow_macroname=true), s) for s in ssyms if completes_global(String(s), name)]
×
UNCOV
145
    appendmacro!(syms, macros, "_str", "\"")
×
UNCOV
146
    appendmacro!(syms, macros, "_cmd", "`")
×
UNCOV
147
    for sym in syms
×
UNCOV
148
        push!(suggestions, ModuleCompletion(mod, sym))
×
UNCOV
149
    end
×
UNCOV
150
    return suggestions
×
151
end
152

153
# REPL Symbol Completions
UNCOV
154
function complete_symbol!(suggestions::Vector{Completion},
×
155
                          @nospecialize(prefix), name::String, context_module::Module;
156
                          complete_modules_only::Bool=false,
157
                          shift::Bool=false)
UNCOV
158
    local mod, t, val
×
159
    complete_internal_only = false
×
UNCOV
160
    if prefix !== nothing
×
UNCOV
161
        res = repl_eval_ex(prefix, context_module)
×
UNCOV
162
        res === nothing && return Completion[]
×
UNCOV
163
        if res isa Const
×
UNCOV
164
            val = res.val
×
UNCOV
165
            if isa(val, Module)
×
UNCOV
166
                mod = val
×
UNCOV
167
                if !shift
×
168
                    # when module is explicitly accessed, show internal bindings that are
169
                    # defined by the module, unless shift key is pressed
170
                    complete_internal_only = true
×
171
                end
172
            else
UNCOV
173
                t = typeof(val)
×
174
            end
175
        else
UNCOV
176
            t = CC.widenconst(res)
×
177
        end
178
    else
UNCOV
179
        mod = context_module
×
180
    end
181

UNCOV
182
    if @isdefined(mod) # lookup names available within the module
×
UNCOV
183
        let modname = nameof(mod),
×
184
            is_main = mod===Main
UNCOV
185
            append_filtered_mod_names!(suggestions, mod, name, complete_internal_only) do s::Symbol
×
UNCOV
186
                if Base.isdeprecated(mod, s)
×
187
                    return false
×
UNCOV
188
                elseif s === modname
×
UNCOV
189
                    return false # exclude `Main.Main.Main`, etc.
×
UNCOV
190
                elseif complete_modules_only && !completes_module(mod, s)
×
UNCOV
191
                    return false
×
UNCOV
192
                elseif is_main && s === :MainInclude
×
UNCOV
193
                    return false
×
194
                end
UNCOV
195
                return true
×
196
            end
197
        end
UNCOV
198
    elseif @isdefined(val) # looking for a property of an instance
×
UNCOV
199
        try
×
UNCOV
200
            for property in propertynames(val, false)
×
201
                # TODO: support integer arguments (#36872)
UNCOV
202
                if property isa Symbol && startswith(string(property), name)
×
UNCOV
203
                    push!(suggestions, PropertyCompletion(val, property))
×
204
                end
UNCOV
205
            end
×
UNCOV
206
        catch
×
207
        end
UNCOV
208
    elseif @isdefined(t) && field_completion_eligible(t)
×
209
        # Looking for a member of a type
UNCOV
210
        add_field_completions!(suggestions, name, t)
×
211
    end
UNCOV
212
    return suggestions
×
213
end
214

UNCOV
215
completes_module(mod::Module, x::Symbol) =
×
216
    Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getglobal(mod, x), Module)
217

UNCOV
218
function add_field_completions!(suggestions::Vector{Completion}, name::String, @nospecialize(t))
×
UNCOV
219
    if isa(t, Union)
×
UNCOV
220
        add_field_completions!(suggestions, name, t.a)
×
UNCOV
221
        add_field_completions!(suggestions, name, t.b)
×
222
    else
UNCOV
223
        @assert isconcretetype(t)
×
UNCOV
224
        fields = fieldnames(t)
×
UNCOV
225
        for field in fields
×
UNCOV
226
            isa(field, Symbol) || continue # Tuple type has ::Int field name
×
UNCOV
227
            s = string(field)
×
UNCOV
228
            if startswith(s, name)
×
UNCOV
229
                push!(suggestions, FieldCompletion(t, field))
×
230
            end
UNCOV
231
        end
×
232
    end
233
end
234

235
const GENERIC_PROPERTYNAMES_METHOD = which(propertynames, (Any,))
236

UNCOV
237
function field_completion_eligible(@nospecialize t)
×
UNCOV
238
    if isa(t, Union)
×
UNCOV
239
        return field_completion_eligible(t.a) && field_completion_eligible(t.b)
×
240
    end
UNCOV
241
    isconcretetype(t) || return false
×
242
    # field completion is correct only when `getproperty` fallbacks to `getfield`
UNCOV
243
    match = Base._which(Tuple{typeof(propertynames),t}; raise=false)
×
UNCOV
244
    match === nothing && return false
×
UNCOV
245
    return match.method === GENERIC_PROPERTYNAMES_METHOD
×
246
end
247

UNCOV
248
function complete_from_list!(suggestions::Vector{Completion}, T::Type, list::Vector{String}, s::String)
×
UNCOV
249
    r = searchsorted(list, s)
×
UNCOV
250
    i = first(r)
×
UNCOV
251
    n = length(list)
×
UNCOV
252
    while i <= n && startswith(list[i],s)
×
UNCOV
253
        r = first(r):i
×
UNCOV
254
        i += 1
×
UNCOV
255
    end
×
UNCOV
256
    for kw in list[r]
×
UNCOV
257
        push!(suggestions, T(kw))
×
UNCOV
258
    end
×
UNCOV
259
    return suggestions
×
260
end
261

262
const sorted_keywords = [
263
    "abstract type", "baremodule", "begin", "break", "catch", "ccall",
264
    "const", "continue", "do", "else", "elseif", "end", "export",
265
    "finally", "for", "function", "global", "if", "import",
266
    "let", "local", "macro", "module", "mutable struct",
267
    "primitive type", "quote", "return", "struct",
268
    "try", "using", "while"]
269

UNCOV
270
complete_keyword!(suggestions::Vector{Completion}, s::String) =
×
271
    complete_from_list!(suggestions, KeywordCompletion, sorted_keywords, s)
272

273
const sorted_keyvals = ["false", "true"]
274

UNCOV
275
complete_keyval!(suggestions::Vector{Completion}, s::String) =
×
276
    complete_from_list!(suggestions, KeyvalCompletion, sorted_keyvals, s)
277

UNCOV
278
function do_raw_escape(s)
×
279
    # escape_raw_string with delim='`' and ignoring the rule for the ending \
280
    return replace(s, r"(\\+)`" => s"\1\\`")
×
281
end
UNCOV
282
function do_shell_escape(s)
×
UNCOV
283
    return Base.shell_escape_posixly(s)
×
284
end
UNCOV
285
function do_string_escape(s)
×
UNCOV
286
    return escape_string(s, ('\"','$'))
×
287
end
288

289
const PATH_cache_lock = Base.ReentrantLock()
290
const PATH_cache = Set{String}()
291
PATH_cache_task::Union{Task,Nothing} = nothing # used for sync in tests
292
next_cache_update::Float64 = 0.0
UNCOV
293
function maybe_spawn_cache_PATH()
×
294
    global PATH_cache_task, next_cache_update
×
UNCOV
295
    @lock PATH_cache_lock begin
×
UNCOV
296
        PATH_cache_task isa Task && !istaskdone(PATH_cache_task) && return
×
UNCOV
297
        time() < next_cache_update && return
×
UNCOV
298
        PATH_cache_task = Threads.@spawn REPLCompletions.cache_PATH()
×
UNCOV
299
        Base.errormonitor(PATH_cache_task)
×
300
    end
301
end
302

303
# caches all reachable files in PATH dirs
UNCOV
304
function cache_PATH()
×
UNCOV
305
    path = get(ENV, "PATH", nothing)
×
UNCOV
306
    path isa String || return
×
307

308
    global next_cache_update
×
309

310
    # Calling empty! on PATH_cache would be annoying for async typing hints as completions would temporarily disappear.
311
    # 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.
UNCOV
312
    this_PATH_cache = Set{String}()
×
313

UNCOV
314
    @debug "caching PATH files" PATH=path
×
UNCOV
315
    pathdirs = split(path, @static Sys.iswindows() ? ";" : ":")
×
316

UNCOV
317
    next_yield_time = time() + 0.01
×
318

UNCOV
319
    t = @elapsed for pathdir in pathdirs
×
UNCOV
320
        actualpath = try
×
UNCOV
321
            realpath(pathdir)
×
322
        catch ex
UNCOV
323
            ex isa Base.IOError || rethrow()
×
324
            # Bash doesn't expect every folder in PATH to exist, so neither shall we
UNCOV
325
            continue
×
326
        end
327

UNCOV
328
        if actualpath != pathdir && in(actualpath, pathdirs)
×
329
            # Remove paths which (after resolving links) are in the env path twice.
330
            # Many distros eg. point /bin to /usr/bin but have both in the env path.
UNCOV
331
            continue
×
332
        end
333

UNCOV
334
        path_entries = try
×
UNCOV
335
            _readdirx(pathdir)
×
336
        catch e
337
            # Bash allows dirs in PATH that can't be read, so we should as well.
UNCOV
338
            if isa(e, Base.IOError) || isa(e, Base.ArgumentError)
×
UNCOV
339
                continue
×
340
            else
341
                # We only handle IOError and ArgumentError here
342
                rethrow()
×
343
            end
344
        end
UNCOV
345
        for entry in path_entries
×
346
            # In a perfect world, we would filter on whether the file is executable
347
            # here, or even on whether the current user can execute the file in question.
UNCOV
348
            try
×
UNCOV
349
                if isfile(entry)
×
UNCOV
350
                    @lock PATH_cache_lock push!(PATH_cache, entry.name)
×
UNCOV
351
                    push!(this_PATH_cache, entry.name)
×
352
                end
353
            catch e
354
                # `isfile()` can throw in rare cases such as when probing a
355
                # symlink that points to a file within a directory we do not
356
                # have read access to.
UNCOV
357
                if isa(e, Base.IOError)
×
UNCOV
358
                    continue
×
359
                else
360
                    rethrow()
×
361
                end
362
            end
UNCOV
363
            if time() >= next_yield_time
×
364
                yield() # to avoid blocking typing when -t1
×
365
                next_yield_time = time() + 0.01
×
366
            end
UNCOV
367
        end
×
UNCOV
368
    end
×
369

UNCOV
370
    @lock PATH_cache_lock begin
×
UNCOV
371
        intersect!(PATH_cache, this_PATH_cache) # remove entries from PATH_cache that weren't found this time
×
UNCOV
372
        next_cache_update = time() + 10 # earliest next update can run is 10s after
×
373
    end
374

UNCOV
375
    @debug "caching PATH files took $t seconds" length(pathdirs) length(PATH_cache)
×
UNCOV
376
    return PATH_cache
×
377
end
378

UNCOV
379
function complete_path(path::AbstractString;
×
380
                       use_envpath=false,
381
                       shell_escape=false,
382
                       raw_escape=false,
383
                       string_escape=false,
384
                       contract_user=false)
UNCOV
385
    @assert !(shell_escape && string_escape)
×
UNCOV
386
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
387
        # if the path is just "~", don't consider the expanded username as a prefix
388
        if path == "~"
×
389
            dir, prefix = homedir(), ""
×
390
        else
391
            dir, prefix = splitdir(homedir() * path[2:end])
×
392
        end
393
    else
UNCOV
394
        dir, prefix = splitdir(path)
×
395
    end
UNCOV
396
    entries = try
×
UNCOV
397
        if isempty(dir)
×
UNCOV
398
            _readdirx()
×
UNCOV
399
        elseif isdir(dir)
×
UNCOV
400
            _readdirx(dir)
×
401
        else
UNCOV
402
            return Completion[], dir, false
×
403
        end
404
    catch ex
405
        ex isa Base.IOError || rethrow()
×
406
        return Completion[], dir, false
×
407
    end
408

UNCOV
409
    matches = Set{String}()
×
UNCOV
410
    for entry in entries
×
UNCOV
411
        if startswith(entry.name, prefix)
×
UNCOV
412
            is_dir = try isdir(entry) catch ex; ex isa Base.IOError ? false : rethrow() end
×
UNCOV
413
            push!(matches, is_dir ? entry.name * "/" : entry.name)
×
414
        end
UNCOV
415
    end
×
416

UNCOV
417
    if use_envpath && isempty(dir)
×
418
        # Look for files in PATH as well. These are cached in `cache_PATH` in an async task to not block typing.
419
        # If we cannot get lock because its still caching just pass over this so that typing isn't laggy.
UNCOV
420
        maybe_spawn_cache_PATH() # only spawns if enough time has passed and the previous caching task has completed
×
UNCOV
421
        @lock PATH_cache_lock begin
×
UNCOV
422
            for file in PATH_cache
×
UNCOV
423
                startswith(file, prefix) && push!(matches, file)
×
UNCOV
424
            end
×
425
        end
426
    end
427

UNCOV
428
    matches = ((shell_escape ? do_shell_escape(s) : string_escape ? do_string_escape(s) : s) for s in matches)
×
429
    matches = ((raw_escape ? do_raw_escape(s) : s) for s in matches)
×
UNCOV
430
    matches = Completion[PathCompletion(contract_user ? contractuser(s) : s) for s in matches]
×
UNCOV
431
    return matches, dir, !isempty(matches)
×
432
end
433

434
function complete_path(path::AbstractString,
×
435
                       pos::Int;
436
                       use_envpath=false,
437
                       shell_escape=false,
438
                       string_escape=false,
439
                       contract_user=false)
440
    ## TODO: enable this depwarn once Pkg is fixed
441
    #Base.depwarn("complete_path with pos argument is deprecated because the return value [2] is incorrect to use", :complete_path)
442
    paths, dir, success = complete_path(path; use_envpath, shell_escape, string_escape)
×
443
    if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
×
444
        # if the path is just "~", don't consider the expanded username as a prefix
445
        if path == "~"
×
446
            dir, prefix = homedir(), ""
×
447
        else
448
            dir, prefix = splitdir(homedir() * path[2:end])
×
449
        end
450
    else
451
        dir, prefix = splitdir(path)
×
452
    end
453
    startpos = pos - lastindex(prefix) + 1
×
454
    Sys.iswindows() && map!(paths, paths) do c::PathCompletion
×
455
        # emulation for unnecessarily complicated return value, since / is a
456
        # perfectly acceptable path character which does not require quoting
457
        # but is required by Pkg's awkward parser handling
458
        return endswith(c.path, "/") ? PathCompletion(chop(c.path) * "\\\\") : c
×
459
    end
460
    return paths, startpos:pos, success
×
461
end
462

UNCOV
463
function complete_expanduser(path::AbstractString, r)
×
UNCOV
464
    expanded =
×
UNCOV
465
        try expanduser(path)
×
466
        catch e
UNCOV
467
            e isa ArgumentError || rethrow()
×
UNCOV
468
            path
×
469
        end
UNCOV
470
    return Completion[PathCompletion(expanded)], r, path != expanded
×
471
end
472

473
# Returns a range that includes the method name in front of the first non
474
# closed start brace from the end of the string.
UNCOV
475
function find_start_brace(s::AbstractString; c_start='(', c_end=')')
×
UNCOV
476
    r = reverse(s)
×
477
    i = firstindex(r)
×
478
    braces = in_comment = 0
×
479
    in_single_quotes = in_double_quotes = in_back_ticks = false
×
UNCOV
480
    while i <= ncodeunits(r)
×
UNCOV
481
        c, i = iterate(r, i)
×
UNCOV
482
        if c == '#' && i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
UNCOV
483
            c, i = iterate(r, i) # consume '='
×
484
            new_comments = 1
×
485
            # handle #=#=#=#, by counting =# pairs
UNCOV
486
            while i <= ncodeunits(r) && iterate(r, i)[1] == '#'
×
UNCOV
487
                c, i = iterate(r, i) # consume '#'
×
UNCOV
488
                iterate(r, i)[1] == '=' || break
×
UNCOV
489
                c, i = iterate(r, i) # consume '='
×
UNCOV
490
                new_comments += 1
×
UNCOV
491
            end
×
UNCOV
492
            if c == '='
×
UNCOV
493
                in_comment += new_comments
×
494
            else
UNCOV
495
                in_comment -= new_comments
×
496
            end
UNCOV
497
        elseif !in_single_quotes && !in_double_quotes && !in_back_ticks && in_comment == 0
×
UNCOV
498
            if c == c_start
×
UNCOV
499
                braces += 1
×
UNCOV
500
            elseif c == c_end
×
UNCOV
501
                braces -= 1
×
UNCOV
502
            elseif c == '\''
×
503
                in_single_quotes = true
×
UNCOV
504
            elseif c == '"'
×
505
                in_double_quotes = true
×
UNCOV
506
            elseif c == '`'
×
507
                in_back_ticks = true
×
508
            end
509
        else
UNCOV
510
            if in_single_quotes &&
×
511
                c == '\'' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
512
                in_single_quotes = false
×
UNCOV
513
            elseif in_double_quotes &&
×
514
                c == '"' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
515
                in_double_quotes = false
×
UNCOV
516
            elseif in_back_ticks &&
×
517
                c == '`' && i <= ncodeunits(r) && iterate(r, i)[1] != '\\'
518
                in_back_ticks = false
×
UNCOV
519
            elseif in_comment > 0 &&
×
520
                c == '=' && i <= ncodeunits(r) && iterate(r, i)[1] == '#'
521
                # handle =#=#=#=, by counting #= pairs
UNCOV
522
                c, i = iterate(r, i) # consume '#'
×
523
                old_comments = 1
×
UNCOV
524
                while i <= ncodeunits(r) && iterate(r, i)[1] == '='
×
UNCOV
525
                    c, i = iterate(r, i) # consume '='
×
UNCOV
526
                    iterate(r, i)[1] == '#' || break
×
UNCOV
527
                    c, i = iterate(r, i) # consume '#'
×
UNCOV
528
                    old_comments += 1
×
UNCOV
529
                end
×
UNCOV
530
                if c == '#'
×
UNCOV
531
                    in_comment -= old_comments
×
532
                else
UNCOV
533
                    in_comment += old_comments
×
534
                end
535
            end
536
        end
UNCOV
537
        braces == 1 && break
×
UNCOV
538
    end
×
UNCOV
539
    braces != 1 && return 0:-1, -1
×
UNCOV
540
    method_name_end = reverseind(s, i)
×
UNCOV
541
    startind = nextind(s, something(findprev(in(non_identifier_chars), s, method_name_end), 0))::Int
×
UNCOV
542
    return (startind:lastindex(s), method_name_end)
×
543
end
544

545
struct REPLCacheToken end
546

547
struct REPLInterpreter <: CC.AbstractInterpreter
548
    limit_aggressive_inference::Bool
549
    world::UInt
550
    inf_params::CC.InferenceParams
551
    opt_params::CC.OptimizationParams
552
    inf_cache::Vector{CC.InferenceResult}
553
    function REPLInterpreter(limit_aggressive_inference::Bool=false;
1✔
554
                             world::UInt = Base.get_world_counter(),
555
                             inf_params::CC.InferenceParams = CC.InferenceParams(;
556
                                 aggressive_constant_propagation=true,
557
                                 unoptimize_throw_blocks=false),
558
                             opt_params::CC.OptimizationParams = CC.OptimizationParams(),
559
                             inf_cache::Vector{CC.InferenceResult} = CC.InferenceResult[])
560
        return new(limit_aggressive_inference, world, inf_params, opt_params, inf_cache)
1✔
561
    end
562
end
563
CC.InferenceParams(interp::REPLInterpreter) = interp.inf_params
1✔
564
CC.OptimizationParams(interp::REPLInterpreter) = interp.opt_params
×
565
CC.get_inference_world(interp::REPLInterpreter) = interp.world
1✔
UNCOV
566
CC.get_inference_cache(interp::REPLInterpreter) = interp.inf_cache
×
567
CC.cache_owner(::REPLInterpreter) = REPLCacheToken()
×
568

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

572
# REPLInterpreter doesn't need any sources to be cached, so discard them aggressively
573
CC.transform_result_for_cache(::REPLInterpreter, ::Core.MethodInstance, ::CC.WorldRange, ::CC.InferenceResult) = nothing
×
574

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

578
# `REPLInterpreter` aggressively resolves global bindings to enable reasonable completions
579
# for lines like `Mod.a.|` (where `|` is the cursor position).
580
# Aggressive binding resolution poses challenges for the inference cache validation
581
# (until https://github.com/JuliaLang/julia/issues/40399 is implemented).
582
# To avoid the cache validation issues, `REPLInterpreter` only allows aggressive binding
583
# resolution for top-level frame representing REPL input code and for child uncached frames
584
# that are constant propagated from the top-level frame ("repl-frame"s). This works, even if
585
# those global bindings are not constant and may be mutated in the future, since:
586
# a.) "repl-frame"s are never cached, and
587
# b.) mutable values are never observed by any cached frames.
588
#
589
# `REPLInterpreter` also aggressively concrete evaluate `:inconsistent` calls within
590
# "repl-frame" to provide reasonable completions for lines like `Ref(Some(42))[].|`.
591
# Aggressive concrete evaluation allows us to get accurate type information about complex
592
# expressions that otherwise can not be constant folded, in a safe way, i.e. it still
593
# doesn't evaluate effectful expressions like `pop!(xs)`.
594
# Similarly to the aggressive binding resolution, aggressive concrete evaluation doesn't
595
# present any cache validation issues because "repl-frame" is never cached.
596

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

UNCOV
602
function is_call_graph_uncached(sv::CC.InferenceState)
×
603
    CC.is_cached(sv) && return false
1✔
604
    parent = sv.parent
1✔
605
    parent === nothing && return true
1✔
UNCOV
606
    return is_call_graph_uncached(parent::CC.InferenceState)
×
607
end
608

609
# aggressive global binding resolution within `repl_frame`
610
function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef,
611
                                    sv::CC.InferenceState)
612
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
2✔
613
        if CC.isdefined_globalref(g)
1✔
614
            return CC.RTEffects(Const(ccall(:jl_get_globalref_value, Any, (Any,), g)), Union{}, CC.EFFECTS_TOTAL)
1✔
615
        end
UNCOV
616
        return CC.RTEffects(Union{}, UndefVarError, CC.EFFECTS_THROWS)
×
617
    end
UNCOV
618
    return @invoke CC.abstract_eval_globalref(interp::CC.AbstractInterpreter, g::GlobalRef,
×
619
                                              sv::CC.InferenceState)
620
end
621

UNCOV
622
function is_repl_frame_getproperty(sv::CC.InferenceState)
×
UNCOV
623
    def = sv.linfo.def
×
UNCOV
624
    def isa Method || return false
×
UNCOV
625
    def.name === :getproperty || return false
×
UNCOV
626
    CC.is_cached(sv) && return false
×
UNCOV
627
    return is_repl_frame(sv.parent)
×
628
end
629

630
# aggressive global binding resolution for `getproperty(::Module, ::Symbol)` calls within `repl_frame`
UNCOV
631
function CC.builtin_tfunction(interp::REPLInterpreter, @nospecialize(f),
×
632
                              argtypes::Vector{Any}, sv::CC.InferenceState)
UNCOV
633
    if f === Core.getglobal && (interp.limit_aggressive_inference ? is_repl_frame_getproperty(sv) : is_call_graph_uncached(sv))
×
UNCOV
634
        if length(argtypes) == 2
×
UNCOV
635
            a1, a2 = argtypes
×
UNCOV
636
            if isa(a1, Const) && isa(a2, Const)
×
UNCOV
637
                a1val, a2val = a1.val, a2.val
×
UNCOV
638
                if isa(a1val, Module) && isa(a2val, Symbol)
×
UNCOV
639
                    g = GlobalRef(a1val, a2val)
×
UNCOV
640
                    if CC.isdefined_globalref(g)
×
UNCOV
641
                        return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
×
642
                    end
643
                    return Union{}
×
644
                end
645
            end
646
        end
647
    end
UNCOV
648
    return @invoke CC.builtin_tfunction(interp::CC.AbstractInterpreter, f::Any,
×
649
                                        argtypes::Vector{Any}, sv::CC.InferenceState)
650
end
651

652
# aggressive concrete evaluation for `:inconsistent` frames within `repl_frame`
UNCOV
653
function CC.concrete_eval_eligible(interp::REPLInterpreter, @nospecialize(f),
×
654
                                   result::CC.MethodCallResult, arginfo::CC.ArgInfo,
655
                                   sv::CC.InferenceState)
UNCOV
656
    if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
×
UNCOV
657
        neweffects = CC.Effects(result.effects; consistent=CC.ALWAYS_TRUE)
×
UNCOV
658
        result = CC.MethodCallResult(result.rt, result.exct, result.edgecycle, result.edgelimited,
×
659
                                     result.edge, neweffects)
660
    end
UNCOV
661
    ret = @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any,
×
662
                                            result::CC.MethodCallResult, arginfo::CC.ArgInfo,
663
                                            sv::CC.InferenceState)
UNCOV
664
    if ret === :semi_concrete_eval
×
665
        # while the base eligibility check probably won't permit semi-concrete evaluation
666
        # for `REPLInterpreter` (given it completely turns off optimization),
667
        # this ensures we don't inadvertently enter irinterp
668
        ret = :none
×
669
    end
UNCOV
670
    return ret
×
671
end
672

673
# allow constant propagation for mutable constants
674
function CC.const_prop_argument_heuristic(interp::REPLInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
UNCOV
675
    if !interp.limit_aggressive_inference
×
UNCOV
676
        any(@nospecialize(a)->isa(a, Const), arginfo.argtypes) && return true # even if mutable
×
677
    end
UNCOV
678
    return @invoke CC.const_prop_argument_heuristic(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
×
679
end
680

681
function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module)
682
    @ccall jl_resolve_globals_in_ir(
1✔
683
        #=jl_array_t *stmts=# src.code::Any,
684
        #=jl_module_t *m=# mod::Any,
685
        #=jl_svec_t *sparam_vals=# Core.svec()::Any,
686
        #=int binding_effects=# 0::Int)::Cvoid
687
    return src
×
688
end
689

690
# lower `ex` and run type inference on the resulting top-level expression
691
function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressive_inference::Bool=false)
2✔
692
    if (isexpr(ex, :toplevel) || isexpr(ex, :tuple)) && !isempty(ex.args)
2✔
693
        # get the inference result for the last expression
UNCOV
694
        ex = ex.args[end]
×
695
    end
696
    lwr = try
1✔
697
        Meta.lower(context_module, ex)
1✔
698
    catch # macro expansion failed, etc.
UNCOV
699
        return nothing
×
700
    end
701
    if lwr isa Symbol
1✔
UNCOV
702
        return isdefined(context_module, lwr) ? Const(getfield(context_module, lwr)) : nothing
×
703
    end
704
    lwr isa Expr || return Const(lwr) # `ex` is literal
1✔
705
    isexpr(lwr, :thunk) || return nothing # lowered to `Expr(:error, ...)` or similar
1✔
706
    src = lwr.args[1]::Core.CodeInfo
1✔
707

708
    resolve_toplevel_symbols!(src, context_module)
1✔
709
    # construct top-level `MethodInstance`
710
    mi = ccall(:jl_method_instance_for_thunk, Ref{Core.MethodInstance}, (Any, Any), src, context_module)
1✔
711

712
    interp = REPLInterpreter(limit_aggressive_inference)
1✔
713
    result = CC.InferenceResult(mi)
1✔
714
    frame = CC.InferenceState(result, src, #=cache=#:no, interp)
1✔
715

716
    # NOTE Use the fixed world here to make `REPLInterpreter` robust against
717
    #      potential invalidations of `Core.Compiler` methods.
718
    Base.invoke_in_world(COMPLETION_WORLD[], CC.typeinf, interp, frame)
1✔
719

720
    result = frame.result.result
1✔
721
    result === Union{} && return nothing # for whatever reason, callers expect this as the Bottom and/or Top type instead
1✔
722
    return result
1✔
723
end
724

725
# `COMPLETION_WORLD[]` will be initialized within `__init__`
726
# (to allow us to potentially remove REPL from the sysimage in the future).
727
# Note that inference from the `code_typed` call below will use the current world age
728
# rather than `typemax(UInt)`, since `Base.invoke_in_world` uses the current world age
729
# when the given world age is higher than the current one.
730
const COMPLETION_WORLD = Ref{UInt}(typemax(UInt))
731

732
# Generate code cache for `REPLInterpreter` now:
733
# This code cache will be available at the world of `COMPLETION_WORLD`,
734
# assuming no invalidation will happen before initializing REPL.
735
# Once REPL is loaded, `REPLInterpreter` will be resilient against future invalidations.
736
code_typed(CC.typeinf, (REPLInterpreter, CC.InferenceState))
737

738
# Method completion on function call expression that look like :(max(1))
739
MAX_METHOD_COMPLETIONS::Int = 40
740
function _complete_methods(ex_org::Expr, context_module::Module, shift::Bool)
1✔
741
    funct = repl_eval_ex(ex_org.args[1], context_module)
1✔
742
    funct === nothing && return 2, nothing, [], Set{Symbol}()
1✔
743
    funct = CC.widenconst(funct)
1✔
744
    args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex_org, context_module, true, true)
1✔
745
    return kwargs_flag, funct, args_ex, kwargs_ex
1✔
746
end
747

748
function complete_methods(ex_org::Expr, context_module::Module=Main, shift::Bool=false)
1✔
749
    kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex_org, context_module, shift)::Tuple{Int, Any, Vector{Any}, Set{Symbol}}
2✔
750
    out = Completion[]
1✔
751
    kwargs_flag == 2 && return out # one of the kwargs is invalid
1✔
752
    kwargs_flag == 0 && push!(args_ex, Vararg{Any}) # allow more arguments if there is no semicolon
1✔
753
    complete_methods!(out, funct, args_ex, kwargs_ex, shift ? -2 : MAX_METHOD_COMPLETIONS, kwargs_flag == 1)
1✔
754
    return out
1✔
755
end
756

757
MAX_ANY_METHOD_COMPLETIONS::Int = 10
UNCOV
758
function recursive_explore_names!(seen::IdSet, callee_module::Module, initial_module::Module, exploredmodules::IdSet{Module}=IdSet{Module}())
×
UNCOV
759
    push!(exploredmodules, callee_module)
×
UNCOV
760
    for name in names(callee_module; all=true, imported=true)
×
UNCOV
761
        if !Base.isdeprecated(callee_module, name) && !startswith(string(name), '#') && isdefined(initial_module, name)
×
UNCOV
762
            func = getfield(callee_module, name)
×
UNCOV
763
            if !isa(func, Module)
×
UNCOV
764
                funct = Core.Typeof(func)
×
UNCOV
765
                push!(seen, funct)
×
UNCOV
766
            elseif isa(func, Module) && func ∉ exploredmodules
×
UNCOV
767
                recursive_explore_names!(seen, func, initial_module, exploredmodules)
×
768
            end
769
        end
UNCOV
770
    end
×
771
end
UNCOV
772
function recursive_explore_names(callee_module::Module, initial_module::Module)
×
UNCOV
773
    seen = IdSet{Any}()
×
UNCOV
774
    recursive_explore_names!(seen, callee_module, initial_module)
×
775
    seen
×
776
end
777

UNCOV
778
function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool, shift::Bool)
×
UNCOV
779
    out = Completion[]
×
UNCOV
780
    args_ex, kwargs_ex, kwargs_flag = try
×
781
        # this may throw, since we set default_any to false
UNCOV
782
        complete_methods_args(ex_org, context_module, false, false)
×
783
    catch ex
784
        ex isa ArgumentError || rethrow()
×
UNCOV
785
        return out
×
786
    end
UNCOV
787
    kwargs_flag == 2 && return out # one of the kwargs is invalid
×
788

789
    # moreargs determines whether to accept more args, independently of the presence of a
790
    # semicolon for the ".?(" syntax
UNCOV
791
    moreargs && push!(args_ex, Vararg{Any})
×
792

UNCOV
793
    for seen_name in recursive_explore_names(callee_module, callee_module)
×
UNCOV
794
        complete_methods!(out, seen_name, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS, false)
×
UNCOV
795
    end
×
796

UNCOV
797
    if !shift
×
798
        # Filter out methods where all arguments are `Any`
UNCOV
799
        filter!(out) do c
×
UNCOV
800
            isa(c, TextCompletion) && return false
×
UNCOV
801
            isa(c, MethodCompletion) || return true
×
UNCOV
802
            sig = Base.unwrap_unionall(c.method.sig)::DataType
×
UNCOV
803
            return !all(@nospecialize(T) -> T === Any || T === Vararg{Any}, sig.parameters[2:end])
×
804
        end
805
    end
806

UNCOV
807
    return out
×
808
end
809

810
function detect_invalid_kwarg!(kwargs_ex::Vector{Symbol}, @nospecialize(x), kwargs_flag::Int, possible_splat::Bool)
UNCOV
811
    n = isexpr(x, :kw) ? x.args[1] : x
×
UNCOV
812
    if n isa Symbol
×
UNCOV
813
        push!(kwargs_ex, n)
×
UNCOV
814
        return kwargs_flag
×
815
    end
UNCOV
816
    possible_splat && isexpr(x, :...) && return kwargs_flag
×
UNCOV
817
    return 2 # The kwarg is invalid
×
818
end
819

820
function detect_args_kwargs(funargs::Vector{Any}, context_module::Module, default_any::Bool, broadcasting::Bool)
1✔
821
    args_ex = Any[]
1✔
822
    kwargs_ex = Symbol[]
1✔
823
    kwargs_flag = 0
×
824
    # kwargs_flag is:
825
    # * 0 if there is no semicolon and no invalid kwarg
826
    # * 1 if there is a semicolon and no invalid kwarg
827
    # * 2 if there are two semicolons or more, or if some kwarg is invalid, which
828
    #        means that it is not of the form "bar=foo", "bar" or "bar..."
829
    for i in (1+!broadcasting):length(funargs)
2✔
UNCOV
830
        ex = funargs[i]
×
UNCOV
831
        if isexpr(ex, :parameters)
×
UNCOV
832
            kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters
×
UNCOV
833
            for x in ex.args
×
UNCOV
834
                kwargs_flag = detect_invalid_kwarg!(kwargs_ex, x, kwargs_flag, true)
×
UNCOV
835
            end
×
UNCOV
836
        elseif isexpr(ex, :kw)
×
UNCOV
837
            kwargs_flag = detect_invalid_kwarg!(kwargs_ex, ex, kwargs_flag, false)
×
838
        else
UNCOV
839
            if broadcasting
×
840
                # handle broadcasting, but only handle number of arguments instead of
841
                # argument types
UNCOV
842
                push!(args_ex, Any)
×
843
            else
UNCOV
844
                argt = repl_eval_ex(ex, context_module)
×
UNCOV
845
                if argt !== nothing
×
UNCOV
846
                    push!(args_ex, CC.widenconst(argt))
×
UNCOV
847
                elseif default_any
×
UNCOV
848
                    push!(args_ex, Any)
×
849
                else
850
                    throw(ArgumentError("argument not found"))
×
851
                end
852
            end
853
        end
UNCOV
854
    end
×
855
    return args_ex, Set{Symbol}(kwargs_ex), kwargs_flag
1✔
856
end
857

858
is_broadcasting_expr(ex::Expr) = ex.head === :. && isexpr(ex.args[2], :tuple)
1✔
859

860
function complete_methods_args(ex::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool)
861
    if allow_broadcasting && is_broadcasting_expr(ex)
1✔
UNCOV
862
        return detect_args_kwargs((ex.args[2]::Expr).args, context_module, default_any, true)
×
863
    end
864
    return detect_args_kwargs(ex.args, context_module, default_any, false)
1✔
865
end
866

867
function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int, exact_nargs::Bool)
1✔
868
    # Input types and number of arguments
869
    t_in = Tuple{funct, args_ex...}
1✔
870
    m = Base._methods_by_ftype(t_in, nothing, max_method_completions, Base.get_world_counter(),
1✔
871
        #=ambig=# true, Ref(typemin(UInt)), Ref(typemax(UInt)), Ptr{Int32}(C_NULL))
872
    if !isa(m, Vector)
1✔
UNCOV
873
        push!(out, TextCompletion(sprint(Base.show_signature_function, funct) * "( too many methods, use SHIFT-TAB to show )"))
×
UNCOV
874
        return
×
875
    end
876
    for match in m
1✔
877
        # TODO: if kwargs_ex, filter out methods without kwargs?
878
        push!(out, MethodCompletion(match.spec_types, match.method))
1✔
879
    end
1✔
880
    # TODO: filter out methods with wrong number of arguments if `exact_nargs` is set
881
end
882

883
include("latex_symbols.jl")
884
include("emoji_symbols.jl")
885

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

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

898
# Aux function to detect whether we're right after a using or import keyword
UNCOV
899
function get_import_mode(s::String)
×
900
    # match simple cases like `using |` and `import  |`
UNCOV
901
    mod_import_match_simple = match(r"^\b(using|import)\s*$", s)
×
UNCOV
902
    if mod_import_match_simple !== nothing
×
UNCOV
903
        if mod_import_match_simple[1] == "using"
×
UNCOV
904
            return :using_module
×
905
        else
UNCOV
906
            return :import_module
×
907
        end
908
    end
909
    # match module import statements like `using Foo|`, `import Foo, Bar|` and `using Foo.Bar, Baz, |`
UNCOV
910
    mod_import_match = match(r"^\b(using|import)\s+([\w\.]+(?:\s*,\s*[\w\.]+)*),?\s*$", s)
×
UNCOV
911
    if mod_import_match !== nothing
×
UNCOV
912
        if mod_import_match.captures[1] == "using"
×
UNCOV
913
            return :using_module
×
914
        else
UNCOV
915
            return :import_module
×
916
        end
917
    end
918
    # now match explicit name import statements like `using Foo: |` and `import Foo: bar, baz|`
UNCOV
919
    name_import_match = match(r"^\b(using|import)\s+([\w\.]+)\s*:\s*([\w@!\s,]+)$", s)
×
UNCOV
920
    if name_import_match !== nothing
×
UNCOV
921
        if name_import_match[1] == "using"
×
UNCOV
922
            return :using_name
×
923
        else
924
            return :import_name
×
925
        end
926
    end
UNCOV
927
    return nothing
×
928
end
929

UNCOV
930
function close_path_completion(dir, paths, str, pos)
×
UNCOV
931
    length(paths) == 1 || return false  # Only close if there's a single choice...
×
UNCOV
932
    path = (paths[1]::PathCompletion).path
×
UNCOV
933
    path = unescape_string(replace(path, "\\\$"=>"\$"))
×
UNCOV
934
    path = joinpath(dir, path)
×
935
    # ...except if it's a directory...
UNCOV
936
    try
×
UNCOV
937
        isdir(path)
×
938
    catch e
UNCOV
939
        e isa Base.IOError || rethrow() # `path` cannot be determined to be a file
×
940
    end && return false
941
    # ...and except if there's already a " at the cursor.
UNCOV
942
    return lastindex(str) <= pos || str[nextind(str, pos)] != '"'
×
943
end
944

UNCOV
945
function bslash_completions(string::String, pos::Int, hint::Bool=false)
×
UNCOV
946
    slashpos = something(findprev(isequal('\\'), string, pos), 0)
×
UNCOV
947
    if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos &&
×
948
        !(1 < slashpos && (string[prevind(string, slashpos)]=='\\')))
949
        # latex / emoji symbol substitution
UNCOV
950
        s = string[slashpos:pos]
×
UNCOV
951
        latex = get(latex_symbols, s, "")
×
UNCOV
952
        if !isempty(latex) # complete an exact match
×
UNCOV
953
            return (true, (Completion[BslashCompletion(latex)], slashpos:pos, true))
×
UNCOV
954
        elseif occursin(subscript_regex, s)
×
UNCOV
955
            sub = map(c -> subscripts[c], s[3:end])
×
UNCOV
956
            return (true, (Completion[BslashCompletion(sub)], slashpos:pos, true))
×
UNCOV
957
        elseif occursin(superscript_regex, s)
×
UNCOV
958
            sup = map(c -> superscripts[c], s[3:end])
×
UNCOV
959
            return (true, (Completion[BslashCompletion(sup)], slashpos:pos, true))
×
960
        end
UNCOV
961
        emoji = get(emoji_symbols, s, "")
×
UNCOV
962
        if !isempty(emoji)
×
UNCOV
963
            return (true, (Completion[BslashCompletion(emoji)], slashpos:pos, true))
×
964
        end
965
        # return possible matches; these cannot be mixed with regular
966
        # Julian completions as only latex / emoji symbols contain the leading \
UNCOV
967
        if startswith(s, "\\:") # emoji
×
UNCOV
968
            namelist = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols))
×
969
        else # latex
UNCOV
970
            namelist = Iterators.filter(k -> startswith(k, s), keys(latex_symbols))
×
971
        end
UNCOV
972
        return (true, (Completion[BslashCompletion(name) for name in sort!(collect(namelist))], slashpos:pos, true))
×
973
    end
UNCOV
974
    return (false, (Completion[], 0:-1, false))
×
975
end
976

UNCOV
977
function dict_identifier_key(str::String, tag::Symbol, context_module::Module=Main)
×
UNCOV
978
    if tag === :string
×
UNCOV
979
        str_close = str*"\""
×
UNCOV
980
    elseif tag === :cmd
×
UNCOV
981
        str_close = str*"`"
×
982
    else
983
        str_close = str
×
984
    end
UNCOV
985
    frange, end_of_identifier = find_start_brace(str_close, c_start='[', c_end=']')
×
UNCOV
986
    isempty(frange) && return (nothing, nothing, nothing)
×
UNCOV
987
    objstr = str[1:end_of_identifier]
×
UNCOV
988
    objex = Meta.parse(objstr, raise=false, depwarn=false)
×
UNCOV
989
    objt = repl_eval_ex(objex, context_module)
×
UNCOV
990
    isa(objt, Core.Const) || return (nothing, nothing, nothing)
×
UNCOV
991
    obj = objt.val
×
UNCOV
992
    isa(obj, AbstractDict) || return (nothing, nothing, nothing)
×
UNCOV
993
    length(obj)::Int < 1_000_000 || return (nothing, nothing, nothing)
×
UNCOV
994
    begin_of_key = something(findnext(!isspace, str, nextind(str, end_of_identifier) + 1), # +1 for [
×
995
                             lastindex(str)+1)
UNCOV
996
    return (obj, str[begin_of_key:end], begin_of_key)
×
997
end
998

999
# This needs to be a separate non-inlined function, see #19441
UNCOV
1000
@noinline function find_dict_matches(identifier::AbstractDict, partial_key)
×
UNCOV
1001
    matches = String[]
×
UNCOV
1002
    for key in keys(identifier)
×
UNCOV
1003
        rkey = repr(key)
×
UNCOV
1004
        startswith(rkey,partial_key) && push!(matches,rkey)
×
UNCOV
1005
    end
×
UNCOV
1006
    return matches
×
1007
end
1008

1009
# Identify an argument being completed in a method call. If the argument is empty, method
1010
# suggestions will be provided instead of argument completions.
UNCOV
1011
function identify_possible_method_completion(partial, last_idx)
×
UNCOV
1012
    fail = 0:-1, Expr(:nothing), 0:-1, 0
×
1013

1014
    # First, check that the last punctuation is either ',', ';' or '('
UNCOV
1015
    idx_last_punct = something(findprev(x -> ispunct(x) && x != '_' && x != '!', partial, last_idx), 0)::Int
×
UNCOV
1016
    idx_last_punct == 0 && return fail
×
UNCOV
1017
    last_punct = partial[idx_last_punct]
×
UNCOV
1018
    last_punct == ',' || last_punct == ';' || last_punct == '(' || return fail
×
1019

1020
    # Then, check that `last_punct` is only followed by an identifier or nothing
UNCOV
1021
    before_last_word_start = something(findprev(in(non_identifier_chars), partial, last_idx), 0)
×
UNCOV
1022
    before_last_word_start == 0 && return fail
×
UNCOV
1023
    all(isspace, @view partial[nextind(partial, idx_last_punct):before_last_word_start]) || return fail
×
1024

1025
    # Check that `last_punct` is either the last '(' or placed after a previous '('
UNCOV
1026
    frange, method_name_end = find_start_brace(@view partial[1:idx_last_punct])
×
UNCOV
1027
    method_name_end ∈ frange || return fail
×
1028

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

1034
    # `wordrange` is the position of the last argument to complete
UNCOV
1035
    wordrange = nextind(partial, before_last_word_start):last_idx
×
UNCOV
1036
    return frange, ex, wordrange, method_name_end
×
1037
end
1038

1039
# Provide completion for keyword arguments in function calls
UNCOV
1040
function complete_keyword_argument(partial::String, last_idx::Int, context_module::Module;
×
1041
                                   shift::Bool=false)
UNCOV
1042
    frange, ex, wordrange, = identify_possible_method_completion(partial, last_idx)
×
UNCOV
1043
    fail = Completion[], 0:-1, frange
×
UNCOV
1044
    ex.head === :call || is_broadcasting_expr(ex) || return fail
×
1045

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

UNCOV
1049
    methods = Completion[]
×
UNCOV
1050
    complete_methods!(methods, funct, Any[Vararg{Any}], kwargs_ex, -1, kwargs_flag == 1)
×
1051
    # TODO: use args_ex instead of Any[Vararg{Any}] and only provide kwarg completion for
1052
    # method calls compatible with the current arguments.
1053

1054
    # For each method corresponding to the function call, provide completion suggestions
1055
    # for each keyword that starts like the last word and that is not already used
1056
    # previously in the expression. The corresponding suggestion is "kwname=".
1057
    # If the keyword corresponds to an existing name, also include "kwname" as a suggestion
1058
    # since the syntax "foo(; kwname)" is equivalent to "foo(; kwname=kwname)".
UNCOV
1059
    last_word = partial[wordrange] # the word to complete
×
UNCOV
1060
    kwargs = Set{String}()
×
UNCOV
1061
    for m in methods
×
UNCOV
1062
        m::MethodCompletion
×
UNCOV
1063
        possible_kwargs = Base.kwarg_decl(m.method)
×
UNCOV
1064
        current_kwarg_candidates = String[]
×
UNCOV
1065
        for _kw in possible_kwargs
×
UNCOV
1066
            kw = String(_kw)
×
UNCOV
1067
            if !endswith(kw, "...") && startswith(kw, last_word) && _kw ∉ kwargs_ex
×
UNCOV
1068
                push!(current_kwarg_candidates, kw)
×
1069
            end
UNCOV
1070
        end
×
UNCOV
1071
        union!(kwargs, current_kwarg_candidates)
×
UNCOV
1072
    end
×
1073

UNCOV
1074
    suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs]
×
1075

1076
    # Only add these if not in kwarg space. i.e. not in `foo(; `
UNCOV
1077
    if kwargs_flag == 0
×
UNCOV
1078
        complete_symbol!(suggestions, #=prefix=#nothing, last_word, context_module; shift)
×
UNCOV
1079
        complete_keyval!(suggestions, last_word)
×
1080
    end
1081

UNCOV
1082
    return sort!(suggestions, by=completion_text), wordrange
×
1083
end
1084

UNCOV
1085
function get_loading_candidates(pkgstarts::String, project_file::String)
×
UNCOV
1086
    loading_candidates = String[]
×
UNCOV
1087
    d = Base.parsed_toml(project_file)
×
UNCOV
1088
    pkg = get(d, "name", nothing)::Union{String, Nothing}
×
UNCOV
1089
    if pkg !== nothing && startswith(pkg, pkgstarts)
×
UNCOV
1090
        push!(loading_candidates, pkg)
×
1091
    end
UNCOV
1092
    deps = get(d, "deps", nothing)::Union{Dict{String, Any}, Nothing}
×
UNCOV
1093
    if deps !== nothing
×
UNCOV
1094
        for (pkg, _) in deps
×
UNCOV
1095
            startswith(pkg, pkgstarts) && push!(loading_candidates, pkg)
×
UNCOV
1096
        end
×
1097
    end
UNCOV
1098
    return loading_candidates
×
1099
end
1100

UNCOV
1101
function complete_loading_candidates!(suggestions::Vector{Completion}, pkgstarts::String, project_file::String)
×
UNCOV
1102
    for name in get_loading_candidates(pkgstarts, project_file)
×
UNCOV
1103
        push!(suggestions, PackageCompletion(name))
×
UNCOV
1104
    end
×
UNCOV
1105
    return suggestions
×
1106
end
1107

UNCOV
1108
function complete_identifiers!(suggestions::Vector{Completion},
×
1109
                               context_module::Module, string::String, name::String,
1110
                               pos::Int, separatorpos::Int, startpos::Int;
1111
                               comp_keywords::Bool=false,
1112
                               complete_modules_only::Bool=false,
1113
                               shift::Bool=false)
UNCOV
1114
    if comp_keywords
×
UNCOV
1115
        complete_keyword!(suggestions, name)
×
UNCOV
1116
        complete_keyval!(suggestions, name)
×
1117
    end
UNCOV
1118
    if separatorpos > 1 && (string[separatorpos] == '.' || string[separatorpos] == ':')
×
UNCOV
1119
        s = string[1:prevind(string, separatorpos)]
×
1120
        # First see if the whole string up to `pos` is a valid expression. If so, use it.
UNCOV
1121
        prefix = Meta.parse(s, raise=false, depwarn=false)
×
UNCOV
1122
        if isexpr(prefix, :incomplete)
×
UNCOV
1123
            s = string[startpos:pos]
×
1124
            # Heuristic to find the start of the expression. TODO: This would be better
1125
            # done with a proper error-recovering parser.
UNCOV
1126
            if 0 < startpos <= lastindex(string) && string[startpos] == '.'
×
UNCOV
1127
                i = prevind(string, startpos)
×
UNCOV
1128
                while 0 < i
×
UNCOV
1129
                    c = string[i]
×
UNCOV
1130
                    if c in (')', ']')
×
1131
                        if c == ')'
×
1132
                            c_start = '('
×
1133
                            c_end = ')'
×
1134
                        elseif c == ']'
×
1135
                            c_start = '['
×
1136
                            c_end = ']'
×
1137
                        end
1138
                        frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
×
1139
                        isempty(frange) && break # unbalanced parens
×
1140
                        startpos = first(frange)
×
1141
                        i = prevind(string, startpos)
×
UNCOV
1142
                    elseif c in ('\'', '\"', '\`')
×
1143
                        s = "$c$c"*string[startpos:pos]
×
1144
                        break
×
1145
                    else
1146
                        break
×
1147
                    end
1148
                    s = string[startpos:pos]
×
1149
                end
×
1150
            end
UNCOV
1151
            if something(findlast(in(non_identifier_chars), s), 0) < something(findlast(isequal('.'), s), 0)
×
UNCOV
1152
                lookup_name, name = rsplit(s, ".", limit=2)
×
UNCOV
1153
                name = String(name)
×
UNCOV
1154
                prefix = Meta.parse(lookup_name, raise=false, depwarn=false)
×
1155
            end
UNCOV
1156
            isexpr(prefix, :incomplete) && (prefix = nothing)
×
UNCOV
1157
        elseif isexpr(prefix, (:using, :import))
×
UNCOV
1158
            arglast = prefix.args[end] # focus on completion to the last argument
×
UNCOV
1159
            if isexpr(arglast, :.)
×
1160
                # We come here for cases like:
1161
                # - `string`: "using Mod1.Mod2.M"
1162
                # - `ex`: :(using Mod1.Mod2)
1163
                # - `name`: "M"
1164
                # Now we transform `ex` to `:(Mod1.Mod2)` to allow `complete_symbol!` to
1165
                # complete for inner modules whose name starts with `M`.
1166
                # Note that `complete_modules_only=true` is set within `completions`
UNCOV
1167
                prefix = nothing
×
1168
                firstdot = true
×
UNCOV
1169
                for arg = arglast.args
×
UNCOV
1170
                    if arg === :.
×
1171
                        # override `context_module` if multiple `.` accessors are used
UNCOV
1172
                        if firstdot
×
1173
                            firstdot = false
×
1174
                        else
UNCOV
1175
                            context_module = parentmodule(context_module)
×
1176
                        end
UNCOV
1177
                    elseif arg isa Symbol
×
UNCOV
1178
                        if prefix === nothing
×
UNCOV
1179
                            prefix = arg
×
1180
                        else
UNCOV
1181
                            prefix = Expr(:., prefix, QuoteNode(arg))
×
1182
                        end
1183
                    else # invalid expression
1184
                        prefix = nothing
×
1185
                        break
×
1186
                    end
UNCOV
1187
                end
×
1188
            end
UNCOV
1189
        elseif isexpr(prefix, :call) && length(prefix.args) > 1
×
UNCOV
1190
            isinfix = s[end] != ')'
×
1191
            # A complete call expression that does not finish with ')' is an infix call.
UNCOV
1192
            if !isinfix
×
1193
                # Handle infix call argument completion of the form bar + foo(qux).
UNCOV
1194
                frange, end_of_identifier = find_start_brace(@view s[1:prevind(s, end)])
×
UNCOV
1195
                isinfix = Meta.parse(@view(s[frange[1]:end]), raise=false, depwarn=false) == prefix.args[end]
×
1196
            end
UNCOV
1197
            if isinfix
×
UNCOV
1198
                prefix = prefix.args[end]
×
1199
            end
UNCOV
1200
        elseif isexpr(prefix, :macrocall) && length(prefix.args) > 1
×
1201
            # allow symbol completions within potentially incomplete macrocalls
UNCOV
1202
            if s[end] ≠ '`' && s[end] ≠ ')'
×
UNCOV
1203
                prefix = prefix.args[end]
×
1204
            end
1205
        end
1206
    else
1207
        prefix = nothing
×
1208
    end
UNCOV
1209
    complete_symbol!(suggestions, prefix, name, context_module; complete_modules_only, shift)
×
UNCOV
1210
    return suggestions
×
1211
end
1212

UNCOV
1213
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false)
×
1214
    # First parse everything up to the current position
UNCOV
1215
    partial = string[1:pos]
×
UNCOV
1216
    inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false))
×
1217

1218
    # ?(x, y)TAB lists methods you can call with these objects
1219
    # ?(x, y TAB lists methods that take these objects as the first two arguments
1220
    # MyModule.?(x, y)TAB restricts the search to names in MyModule
UNCOV
1221
    rexm = match(r"(\w+\.|)\?\((.*)$", partial)
×
UNCOV
1222
    if rexm !== nothing
×
1223
        # Get the module scope
UNCOV
1224
        if isempty(rexm.captures[1])
×
1225
            callee_module = context_module
×
1226
        else
UNCOV
1227
            modname = Symbol(rexm.captures[1][1:end-1])
×
UNCOV
1228
            if isdefined(context_module, modname)
×
UNCOV
1229
                callee_module = getfield(context_module, modname)
×
UNCOV
1230
                if !isa(callee_module, Module)
×
1231
                    callee_module = context_module
×
1232
                end
1233
            else
1234
                callee_module = context_module
×
1235
            end
1236
        end
UNCOV
1237
        moreargs = !endswith(rexm.captures[2], ')')
×
UNCOV
1238
        callstr = "_(" * rexm.captures[2]
×
UNCOV
1239
        if moreargs
×
UNCOV
1240
            callstr *= ')'
×
1241
        end
UNCOV
1242
        ex_org = Meta.parse(callstr, raise=false, depwarn=false)
×
UNCOV
1243
        if isa(ex_org, Expr)
×
UNCOV
1244
            return complete_any_methods(ex_org, callee_module::Module, context_module, moreargs, shift), (0:length(rexm.captures[1])+1) .+ rexm.offset, false
×
1245
        end
1246
    end
1247

1248
    # if completing a key in a Dict
UNCOV
1249
    identifier, partial_key, loc = dict_identifier_key(partial, inc_tag, context_module)
×
UNCOV
1250
    if identifier !== nothing
×
UNCOV
1251
        matches = find_dict_matches(identifier, partial_key)
×
UNCOV
1252
        length(matches)==1 && (lastindex(string) <= pos || string[nextind(string,pos)] != ']') && (matches[1]*=']')
×
UNCOV
1253
        length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true
×
1254
    end
1255

UNCOV
1256
    suggestions = Completion[]
×
1257

1258
    # Check if this is a var"" string macro that should be completed like
1259
    # an identifier rather than a string.
1260
    # TODO: It would be nice for the parser to give us more information here
1261
    # so that we can lookup the macro by identity rather than pattern matching
1262
    # its invocation.
UNCOV
1263
    varrange = findprev("var\"", string, pos)
×
1264

1265
    expanded = nothing
×
UNCOV
1266
    was_expanded = false
×
1267

UNCOV
1268
    if varrange !== nothing
×
UNCOV
1269
        ok, ret = bslash_completions(string, pos)
×
UNCOV
1270
        ok && return ret
×
UNCOV
1271
        startpos = first(varrange) + 4
×
UNCOV
1272
        separatorpos = something(findprev(isequal('.'), string, first(varrange)-1), 0)
×
UNCOV
1273
        name = string[startpos:pos]
×
UNCOV
1274
        complete_identifiers!(suggestions, context_module, string, name,
×
1275
                              pos, separatorpos, startpos;
1276
                              shift)
UNCOV
1277
        return sort!(unique!(completion_text, suggestions), by=completion_text), (separatorpos+1):pos, true
×
UNCOV
1278
    elseif inc_tag === :cmd
×
1279
        # TODO: should this call shell_completions instead of partially reimplementing it?
UNCOV
1280
        let m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial)) # fuzzy shell_parse in reverse
×
UNCOV
1281
            startpos = nextind(partial, reverseind(partial, m.offset))
×
UNCOV
1282
            r = startpos:pos
×
UNCOV
1283
            scs::String = string[r]
×
1284

UNCOV
1285
            expanded = complete_expanduser(scs, r)
×
UNCOV
1286
            was_expanded = expanded[3]
×
UNCOV
1287
            if was_expanded
×
1288
                scs = (only(expanded[1])::PathCompletion).path
×
1289
                # If tab press, ispath and user expansion available, return it now
1290
                # otherwise see if we can complete the path further before returning with expanded ~
1291
                !hint && ispath(scs) && return expanded::Completions
×
1292
            end
1293

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

UNCOV
1300
            if success && !isempty(dir)
×
1301
                let dir = do_raw_escape(do_shell_escape(dir))
×
1302
                    # if escaping of dir matches scs prefix, remove that from the completions
1303
                    # otherwise make it the whole completion
1304
                    if endswith(dir, "/") && startswith(scs, dir)
×
1305
                        r = (startpos + sizeof(dir)):pos
×
1306
                    elseif startswith(scs, dir * "/")
×
1307
                        r = nextind(string, startpos + sizeof(dir)):pos
×
1308
                    else
1309
                        map!(paths, paths) do c::PathCompletion
×
1310
                            p = dir * "/" * c.path
×
1311
                            was_expanded && (p = contractuser(p))
×
1312
                            return PathCompletion(p)
×
1313
                        end
1314
                    end
1315
                end
1316
            end
UNCOV
1317
            if isempty(paths) && !hint && was_expanded
×
1318
                # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
1319
                return expanded::Completions
×
1320
            else
UNCOV
1321
                return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
×
1322
            end
1323
        end
UNCOV
1324
    elseif inc_tag === :string
×
1325
        # Find first non-escaped quote
UNCOV
1326
        let m = match(r"\"(?!\\)", reverse(partial))
×
UNCOV
1327
            startpos = nextind(partial, reverseind(partial, m.offset))
×
UNCOV
1328
            r = startpos:pos
×
UNCOV
1329
            scs::String = string[r]
×
1330

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

UNCOV
1340
            path = try
×
UNCOV
1341
                unescape_string(replace(scs, "\\\$"=>"\$"))
×
1342
            catch ex
1343
                ex isa ArgumentError || rethrow()
×
UNCOV
1344
                nothing
×
1345
            end
UNCOV
1346
            if !isnothing(path)
×
UNCOV
1347
                paths, dir, success = complete_path(path::String, string_escape=true)
×
1348

UNCOV
1349
                if close_path_completion(dir, paths, path, pos)
×
UNCOV
1350
                    p = (paths[1]::PathCompletion).path * "\""
×
UNCOV
1351
                    hint && was_expanded && (p = contractuser(p))
×
UNCOV
1352
                    paths[1] = PathCompletion(p)
×
1353
                end
1354

UNCOV
1355
                if success && !isempty(dir)
×
UNCOV
1356
                    let dir = do_string_escape(dir)
×
1357
                        # if escaping of dir matches scs prefix, remove that from the completions
1358
                        # otherwise make it the whole completion
UNCOV
1359
                        if endswith(dir, "/") && startswith(scs, dir)
×
UNCOV
1360
                            r = (startpos + sizeof(dir)):pos
×
UNCOV
1361
                        elseif startswith(scs, dir * "/") && dir != dirname(homedir())
×
UNCOV
1362
                            was_expanded && (dir = contractuser(dir))
×
UNCOV
1363
                            r = nextind(string, startpos + sizeof(dir)):pos
×
1364
                        else
1365
                            map!(paths, paths) do c::PathCompletion
×
1366
                                p = dir * "/" * c.path
×
1367
                                hint && was_expanded && (p = contractuser(p))
×
1368
                                return PathCompletion(p)
×
1369
                            end
1370
                        end
1371
                    end
1372
                end
1373

1374
                # Fallthrough allowed so that Latex symbols can be completed in strings
UNCOV
1375
                if success
×
UNCOV
1376
                    return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
×
UNCOV
1377
                elseif !hint && was_expanded
×
1378
                    # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
UNCOV
1379
                    return expanded::Completions
×
1380
                end
1381
            end
1382
        end
1383
    end
1384
    # if path has ~ and we didn't find any paths to complete just return the expanded path
UNCOV
1385
    was_expanded && return expanded::Completions
×
1386

UNCOV
1387
    ok, ret = bslash_completions(string, pos)
×
UNCOV
1388
    ok && return ret
×
1389

1390
    # Make sure that only bslash_completions is working on strings
UNCOV
1391
    inc_tag === :string && return Completion[], 0:-1, false
×
UNCOV
1392
    if inc_tag === :other
×
UNCOV
1393
        frange, ex, wordrange, method_name_end = identify_possible_method_completion(partial, pos)
×
UNCOV
1394
        if last(frange) != -1 && all(isspace, @view partial[wordrange]) # no last argument to complete
×
UNCOV
1395
            if ex.head === :call
×
UNCOV
1396
                return complete_methods(ex, context_module, shift), first(frange):method_name_end, false
×
UNCOV
1397
            elseif is_broadcasting_expr(ex)
×
UNCOV
1398
                return complete_methods(ex, context_module, shift), first(frange):(method_name_end - 1), false
×
1399
            end
1400
        end
UNCOV
1401
    elseif inc_tag === :comment
×
UNCOV
1402
        return Completion[], 0:-1, false
×
1403
    end
1404

1405
    # Check whether we can complete a keyword argument in a function call
UNCOV
1406
    kwarg_completion, wordrange = complete_keyword_argument(partial, pos, context_module; shift)
×
UNCOV
1407
    isempty(wordrange) || return kwarg_completion, wordrange, !isempty(kwarg_completion)
×
1408

UNCOV
1409
    startpos = nextind(string, something(findprev(in(non_identifier_chars), string, pos), 0))
×
1410
    # strip preceding ! operator
UNCOV
1411
    if (m = match(r"\G\!+", partial, startpos)) isa RegexMatch
×
UNCOV
1412
        startpos += length(m.match)
×
1413
    end
1414

UNCOV
1415
    separatorpos = something(findprev(isequal('.'), string, pos), 0)
×
UNCOV
1416
    namepos = max(startpos, separatorpos+1)
×
UNCOV
1417
    name = string[namepos:pos]
×
UNCOV
1418
    import_mode = get_import_mode(string)
×
UNCOV
1419
    if import_mode === :using_module || import_mode === :import_module
×
1420
        # Given input lines like `using Foo|`, `import Foo, Bar|` and `using Foo.Bar, Baz, |`:
1421
        # Let's look only for packages and modules we can reach from here
1422

1423
        # If there's no dot, we're in toplevel, so we should
1424
        # also search for packages
UNCOV
1425
        s = string[startpos:pos]
×
UNCOV
1426
        if separatorpos <= startpos
×
UNCOV
1427
            for dir in Base.load_path()
×
UNCOV
1428
                if basename(dir) in Base.project_names && isfile(dir)
×
UNCOV
1429
                    complete_loading_candidates!(suggestions, s, dir)
×
1430
                end
UNCOV
1431
                isdir(dir) || continue
×
UNCOV
1432
                for entry in _readdirx(dir)
×
UNCOV
1433
                    pname = entry.name
×
UNCOV
1434
                    if pname[1] != '.' && pname != "METADATA" &&
×
1435
                        pname != "REQUIRE" && startswith(pname, s)
1436
                        # Valid file paths are
1437
                        #   <Mod>.jl
1438
                        #   <Mod>/src/<Mod>.jl
1439
                        #   <Mod>.jl/src/<Mod>.jl
UNCOV
1440
                        if isfile(entry)
×
1441
                            endswith(pname, ".jl") && push!(suggestions,
×
1442
                                                            PackageCompletion(pname[1:prevind(pname, end-2)]))
1443
                        else
UNCOV
1444
                            mod_name = if endswith(pname, ".jl")
×
1445
                                pname[1:prevind(pname, end-2)]
×
1446
                            else
UNCOV
1447
                                pname
×
1448
                            end
UNCOV
1449
                            if isfile(joinpath(entry, "src",
×
1450
                                               "$mod_name.jl"))
UNCOV
1451
                                push!(suggestions, PackageCompletion(mod_name))
×
1452
                            end
1453
                        end
1454
                    end
UNCOV
1455
                end
×
UNCOV
1456
            end
×
1457
        end
UNCOV
1458
        comp_keywords = false
×
UNCOV
1459
        complete_modules_only = import_mode === :using_module # allow completion for `import Mod.name` (where `name` is not a module)
×
UNCOV
1460
    elseif import_mode === :using_name || import_mode === :import_name
×
1461
        # `using Foo: |` and `import Foo: bar, baz|`
UNCOV
1462
        separatorpos = findprev(isequal(':'), string, pos)::Int
×
1463
        comp_keywords = false
×
1464
        complete_modules_only = false
×
1465
    else
UNCOV
1466
        comp_keywords = !isempty(name) && startpos > separatorpos
×
1467
        complete_modules_only = false
×
1468
    end
1469

UNCOV
1470
    complete_identifiers!(suggestions, context_module, string, name,
×
1471
                          pos, separatorpos, startpos;
1472
                          comp_keywords, complete_modules_only, shift)
UNCOV
1473
    return sort!(unique!(completion_text, suggestions), by=completion_text), namepos:pos, true
×
1474
end
1475

UNCOV
1476
function shell_completions(string, pos, hint::Bool=false)
×
1477
    # First parse everything up to the current position
UNCOV
1478
    scs = string[1:pos]
×
UNCOV
1479
    args, last_arg_start = try
×
UNCOV
1480
        Base.shell_parse(scs, true)::Tuple{Expr,Int}
×
1481
    catch ex
UNCOV
1482
        ex isa ArgumentError || ex isa ErrorException || rethrow()
×
UNCOV
1483
        return Completion[], 0:-1, false
×
1484
    end
UNCOV
1485
    ex = args.args[end]::Expr
×
1486
    # Now look at the last thing we parsed
UNCOV
1487
    isempty(ex.args) && return Completion[], 0:-1, false
×
UNCOV
1488
    lastarg = ex.args[end]
×
1489
    # As Base.shell_parse throws away trailing spaces (unless they are escaped),
1490
    # we need to special case here.
1491
    # If the last char was a space, but shell_parse ignored it search on "".
UNCOV
1492
    if isexpr(lastarg, :incomplete) || isexpr(lastarg, :error)
×
UNCOV
1493
        partial = string[last_arg_start:pos]
×
UNCOV
1494
        ret, range = completions(partial, lastindex(partial), Main, true, hint)
×
UNCOV
1495
        range = range .+ (last_arg_start - 1)
×
UNCOV
1496
        return ret, range, true
×
UNCOV
1497
    elseif endswith(scs, ' ') && !endswith(scs, "\\ ")
×
UNCOV
1498
        r = pos+1:pos
×
UNCOV
1499
        paths, dir, success = complete_path("", use_envpath=false, shell_escape=true)
×
UNCOV
1500
        return paths, r, success
×
UNCOV
1501
    elseif all(@nospecialize(arg) -> arg isa AbstractString, ex.args)
×
1502
        # Join these and treat this as a path
UNCOV
1503
        path::String = join(ex.args)
×
UNCOV
1504
        r = last_arg_start:pos
×
1505

1506
        # Also try looking into the env path if the user wants to complete the first argument
UNCOV
1507
        use_envpath = length(args.args) < 2
×
1508

UNCOV
1509
        expanded = complete_expanduser(path, r)
×
UNCOV
1510
        was_expanded = expanded[3]
×
UNCOV
1511
        if was_expanded
×
UNCOV
1512
            path = (only(expanded[1])::PathCompletion).path
×
1513
            # If tab press, ispath and user expansion available, return it now
1514
            # otherwise see if we can complete the path further before returning with expanded ~
UNCOV
1515
            !hint && ispath(path) && return expanded::Completions
×
1516
        end
1517

UNCOV
1518
        paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true, contract_user=was_expanded)
×
1519

UNCOV
1520
        if success && !isempty(dir)
×
UNCOV
1521
            let dir = do_shell_escape(dir)
×
1522
                # if escaping of dir matches scs prefix, remove that from the completions
1523
                # otherwise make it the whole completion
UNCOV
1524
                partial = string[last_arg_start:pos]
×
UNCOV
1525
                if endswith(dir, "/") && startswith(partial, dir)
×
UNCOV
1526
                    r = (last_arg_start + sizeof(dir)):pos
×
UNCOV
1527
                elseif startswith(partial, dir * "/")
×
UNCOV
1528
                    r = nextind(string, last_arg_start + sizeof(dir)):pos
×
1529
                else
UNCOV
1530
                    map!(paths, paths) do c::PathCompletion
×
UNCOV
1531
                        return PathCompletion(dir * "/" * c.path)
×
1532
                    end
1533
                end
1534
            end
1535
        end
1536
        # if ~ was expanded earlier and the incomplete string isn't a path
1537
        # return the path with contracted user to match what the hint shows. Otherwise expand ~
1538
        # i.e. require two tab presses to expand user
UNCOV
1539
        if was_expanded && !ispath(path)
×
1540
            map!(paths, paths) do c::PathCompletion
×
1541
                PathCompletion(contractuser(c.path))
×
1542
            end
1543
        end
UNCOV
1544
        return paths, r, success
×
1545
    end
UNCOV
1546
    return Completion[], 0:-1, false
×
1547
end
1548

1549
function __init__()
3✔
1550
    COMPLETION_WORLD[] = Base.get_world_counter()
3✔
1551
    return nothing
3✔
1552
end
1553

1554
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