Skip to content

Commit d9da421

Browse files
xal-0KristofferC
authored andcommitted
JuliaSyntax parser-based REPL completions overhaul (#57767)
# Overview As we add REPL features, bugs related to the ad-hoc parsing done by `REPLCompletions.completions` have crept in. This pull request replaces most of the manual parsing (regex, `find_start_brace`) with a new approach that parses the entire input buffer once, before and after the cursor, using JuliaSyntax. We then query the parsed syntax tree to determine the kind of completion to be done. # Changes - New, JuliaSyntax-based completions mechanism. - The `complete_line` interface now has the option of replacing arbitrary regions of text in the input buffer by returning a `Region` (`Pair{Int, Int}` for consistency with the convention in LineEdit, and `pos` being a 0-based byte offset). - Fixes parsing-related bugs: - fix #55420 - fix #55429 - fix #55518 - fix #55520 - fix #55842 - fix #56389 - fix #57307 - fix #57611 - fix #57624 - fix #58099 - Fixes some bugs that exist on 28d3bd5 that were found by fuzzing: - `x \"` + `TAB` throws a `FieldError` exception - String completion would sometimes delete the entire input buffer. - Completions should not happen inside comments. - The duplicate code for path completion in strings, `Cmd`-strings, and the shell has been removed, causing paths to complete the same way for all three. Now, `~` is expanded in two situations: - If `foo` exists, or if `foo` does not exist but there are no possible completions: ``` "~/foo/b|" =TAB=> "~/foo/bar|" "~/foo/bar|" =TAB=> "/home/user/foo/bar|" OR "~/foo/bar"| =TAB=> "/home/user/foo/bar"| ``` - If the current path ends with a `/` and you hit TAB again: ``` "~/foo/|" =TAB=> "/home/user/foo/|" OR "~/foo/"| =TAB=> "/home/user/foo/"| ``` # Future work - Method completions could be changed to look for methods with exactly the given number of arguments if the closing `)` is present, and search for signatures with the right prefix otherwise. - It would be nice to be able to search by type as well as value (perhaps by putting `::T` in place of arguments). - Other REPL features could benefit from JuliaSyntax, so it might be worth sharing the parse tree between completions and other features: - Emacs-style sexpr navigation: `C-M-f`/`C-M-b`/`C-M-u`, etc. - Improved auto-indent. - It would be nice if hints worked even when the cursor is between text. - `CursorNode` is a slightly tweaked copy of `SyntaxNode` from JuliaSyntax that tracks the parent node but includes all trivia. It is used with `seek_pos`, which navigates to the innermost node at a given position so we can examine nearby nodes and the parent. This could probably duplicate less code from JuliaSyntax. (cherry picked from commit ff0a931)
1 parent 8d69739 commit d9da421

File tree

5 files changed

+759
-688
lines changed

5 files changed

+759
-688
lines changed

stdlib/REPL/src/LineEdit.jl

+31-29
Original file line numberDiff line numberDiff line change
@@ -391,16 +391,21 @@ function complete_line(s::MIState)
391391
end
392392
end
393393

394+
# Old complete_line return type: Vector{String}, String, Bool
395+
# New complete_line return type: NamedCompletion{String}, String, Bool
396+
# OR NamedCompletion{String}, Region, Bool
397+
#
394398
# due to close coupling of the Pkg ReplExt `complete_line` can still return a vector of strings,
395399
# so we convert those in this helper
396-
function complete_line_named(args...; kwargs...)::Tuple{Vector{NamedCompletion},String,Bool}
397-
result = complete_line(args...; kwargs...)::Union{Tuple{Vector{NamedCompletion},String,Bool},Tuple{Vector{String},String,Bool}}
398-
if result isa Tuple{Vector{NamedCompletion},String,Bool}
399-
return result
400-
else
401-
completions, partial, should_complete = result
402-
return map(NamedCompletion, completions), partial, should_complete
403-
end
400+
function complete_line_named(c, s, args...; kwargs...)::Tuple{Vector{NamedCompletion},Region,Bool}
401+
r1, r2, should_complete = complete_line(c, s, args...; kwargs...)::Union{
402+
Tuple{Vector{String}, String, Bool},
403+
Tuple{Vector{NamedCompletion}, String, Bool},
404+
Tuple{Vector{NamedCompletion}, Region, Bool},
405+
}
406+
completions = (r1 isa Vector{String} ? map(NamedCompletion, r1) : r1)
407+
r = (r2 isa String ? (position(s)-sizeof(r2) => position(s)) : r2)
408+
completions, r, should_complete
404409
end
405410

406411
# checks for a hint and shows it if appropriate.
@@ -426,14 +431,14 @@ function check_show_hint(s::MIState)
426431
return
427432
end
428433
t_completion = Threads.@spawn :default begin
429-
named_completions, partial, should_complete = nothing, nothing, nothing
434+
named_completions, reg, should_complete = nothing, nothing, nothing
430435

431436
# only allow one task to generate hints at a time and check around lock
432437
# if the user has pressed a key since the hint was requested, to skip old completions
433438
next_key_pressed() && return
434439
@lock s.hint_generation_lock begin
435440
next_key_pressed() && return
436-
named_completions, partial, should_complete = try
441+
named_completions, reg, should_complete = try
437442
complete_line_named(st.p.complete, st, s.active_module; hint = true)
438443
catch
439444
lock_clear_hint()
@@ -448,21 +453,19 @@ function check_show_hint(s::MIState)
448453
return
449454
end
450455
# Don't complete for single chars, given e.g. `x` completes to `xor`
451-
if length(partial) > 1 && should_complete
456+
if reg.second - reg.first > 1 && should_complete
452457
singlecompletion = length(completions) == 1
453458
p = singlecompletion ? completions[1] : common_prefix(completions)
454459
if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
455-
# The completion `p` and the input `partial` may not share the same initial
460+
# The completion `p` and the region `reg` may not share the same initial
456461
# characters, for instance when completing to subscripts or superscripts.
457462
# So, in general, make sure that the hint starts at the correct position by
458463
# incrementing its starting position by as many characters as the input.
459-
startind = 1 # index of p from which to start providing the hint
460-
maxind = ncodeunits(p)
461-
for _ in partial
462-
startind = nextind(p, startind)
463-
startind > maxind && break
464-
end
464+
maxind = lastindex(p)
465+
startind = sizeof(content(s, reg))
465466
if startind maxind # completion on a complete name returns itself so check that there's something to hint
467+
# index of p from which to start providing the hint
468+
startind = nextind(p, startind)
466469
hint = p[startind:end]
467470
next_key_pressed() && return
468471
@lock s.line_modify_lock begin
@@ -491,25 +494,24 @@ function clear_hint(s::ModeState)
491494
end
492495

493496
function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false)
494-
completions, partial, should_complete = complete_line_named(s.p.complete, s, mod; hint)
497+
completions, reg, should_complete = complete_line_named(s.p.complete, s, mod; hint)
495498
isempty(completions) && return false
496499
if !should_complete
497500
# should_complete is false for cases where we only want to show
498501
# a list of possible completions but not complete, e.g. foo(\t
499502
show_completions(s, completions)
500503
elseif length(completions) == 1
501504
# Replace word by completion
502-
prev_pos = position(s)
503505
push_undo(s)
504-
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1].completion)
506+
edit_splice!(s, reg, completions[1].completion)
505507
else
506508
p = common_prefix(completions)
509+
partial = content(s, reg.first => min(bufend(s), reg.first + sizeof(p)))
507510
if !isempty(p) && p != partial
508511
# All possible completions share the same prefix, so we might as
509-
# well complete that
510-
prev_pos = position(s)
512+
# well complete that.
511513
push_undo(s)
512-
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, p)
514+
edit_splice!(s, reg, p)
513515
elseif repeats > 0
514516
show_completions(s, completions)
515517
end
@@ -830,12 +832,12 @@ function edit_move_right(m::MIState)
830832
refresh_line(s)
831833
return true
832834
else
833-
completions, partial, should_complete = complete_line(s.p.complete, s, m.active_module)
834-
if should_complete && eof(buf) && length(completions) == 1 && length(partial) > 1
835+
completions, reg, should_complete = complete_line(s.p.complete, s, m.active_module)
836+
if should_complete && eof(buf) && length(completions) == 1 && reg.second - reg.first > 1
835837
# Replace word by completion
836838
prev_pos = position(s)
837839
push_undo(s)
838-
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1].completion)
840+
edit_splice!(s, (prev_pos - reg.second + reg.first) => prev_pos, completions[1].completion)
839841
refresh_line(state(s))
840842
return true
841843
else
@@ -2255,12 +2257,12 @@ setmodifiers!(c) = nothing
22552257

22562258
# Search Mode completions
22572259
function complete_line(s::SearchState, repeats, mod::Module; hint::Bool=false)
2258-
completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod; hint)
2260+
completions, reg, should_complete = complete_line(s.histprompt.complete, s, mod; hint)
22592261
# For now only allow exact completions in search mode
22602262
if length(completions) == 1
22612263
prev_pos = position(s)
22622264
push_undo(s)
2263-
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1].completion)
2265+
edit_splice!(s, (prev_pos - reg.second - reg.first) => prev_pos, completions[1].completion)
22642266
return true
22652267
end
22662268
return false

stdlib/REPL/src/REPL.jl

+13-10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import .LineEdit:
8686
PromptState,
8787
mode_idx
8888

89+
include("SyntaxUtil.jl")
8990
include("REPLCompletions.jl")
9091
using .REPLCompletions
9192

@@ -799,27 +800,29 @@ end
799800

800801
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
801802

803+
# Convert inclusive-inclusive 1-based char indexing to inclusive-exclusive byte Region.
804+
to_region(s, r) = first(r)-1 => (length(r) > 0 ? nextind(s, last(r))-1 : first(r)-1)
805+
802806
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
803-
partial = beforecursor(s.input_buffer)
804807
full = LineEdit.input_string(s)
805-
ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
808+
ret, range, should_complete = completions(full, thisind(full, position(s)), mod, c.modifiers.shift, hint)
809+
range = to_region(full, range)
806810
c.modifiers = LineEdit.Modifiers()
807-
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
811+
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
808812
end
809813

810814
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
811-
# First parse everything up to the current position
812-
partial = beforecursor(s.input_buffer)
813815
full = LineEdit.input_string(s)
814-
ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
815-
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
816+
ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
817+
range = to_region(full, range)
818+
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
816819
end
817820

818821
function complete_line(c::LatexCompletions, s; hint::Bool=false)
819-
partial = beforecursor(LineEdit.buffer(s))
820822
full = LineEdit.input_string(s)::String
821-
ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
822-
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
823+
ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
824+
range = to_region(full, range)
825+
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
823826
end
824827

825828
with_repl_linfo(f, repl) = f(outstream(repl))

0 commit comments

Comments
 (0)