Skip to content

Commit

Permalink
Actually show glyphs for latex or emoji shortcodes being suggested in…
Browse files Browse the repository at this point in the history
… the REPL (#54800)

When a user requests a completion for a backslash shortcode, this PR
adds the glyphs for all the suggestions to the output. This makes it
much easier to find the result one is looking for, especially if the
user doesn't know all latex and emoji specifiers by heart.

Before:

<img width="813" alt="image"
src="https://github.com/JuliaLang/julia/assets/22495855/bf651399-85a6-4677-abdc-c66a104e3b89">

After:

<img width="977" alt="image"
src="https://github.com/JuliaLang/julia/assets/22495855/04c53ea2-318f-4888-96eb-0215b49c10f3">

---------

Co-authored-by: Dilum Aluthge <[email protected]>
  • Loading branch information
jkrumbiegel and DilumAluthge authored Dec 3, 2024
1 parent 2c87290 commit 9acf112
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 30 deletions.
38 changes: 33 additions & 5 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,18 @@ struct EmptyHistoryProvider <: HistoryProvider end

reset_state(::EmptyHistoryProvider) = nothing

complete_line(c::EmptyCompletionProvider, s; hint::Bool=false) = String[], "", true
# Before, completions were always given as strings. But at least for backslash
# completions, it's nice to see what glyphs are available in the completion preview.
# To separate between what's shown in the preview list of possible matches, and what's
# actually completed, we introduce this struct.
struct NamedCompletion
completion::String # what is actually completed, for example "\trianglecdot"
name::String # what is displayed in lists of possible completions, for example "◬ \trianglecdot"
end

NamedCompletion(completion::String) = NamedCompletion(completion, completion)

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

# complete_line can be specialized for only two arguments, when the active module
# doesn't matter (e.g. Pkg does this)
Expand Down Expand Up @@ -308,6 +319,7 @@ end

set_action!(s, command::Symbol) = nothing

common_prefix(completions::Vector{NamedCompletion}) = common_prefix(map(x -> x.completion, completions))
function common_prefix(completions::Vector{String})
ret = ""
c1 = completions[1]
Expand All @@ -330,6 +342,8 @@ end
# does not restrict column length when multiple columns are used.
const MULTICOLUMN_THRESHOLD = 5

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

# Show available completions
function show_completions(s::PromptState, completions::Vector{String})
# skip any lines of input after the cursor
Expand Down Expand Up @@ -374,6 +388,18 @@ function complete_line(s::MIState)
end
end

# due to close coupling of the Pkg ReplExt `complete_line` can still return a vector of strings,
# so we convert those in this helper
function complete_line_named(args...; kwargs...)::Tuple{Vector{NamedCompletion},String,Bool}
result = complete_line(args...; kwargs...)::Union{Tuple{Vector{NamedCompletion},String,Bool},Tuple{Vector{String},String,Bool}}
if result isa Tuple{Vector{NamedCompletion},String,Bool}
return result
else
completions, partial, should_complete = result
return map(NamedCompletion, completions), partial, should_complete
end
end

function check_for_hint(s::MIState)
st = state(s)
if !options(st).hint_tab_completes || !eof(buffer(st))
Expand All @@ -383,12 +409,14 @@ function check_for_hint(s::MIState)
return clear_hint(st)
end

completions, partial, should_complete = try
complete_line(st.p.complete, st, s.active_module; hint = true)::Tuple{Vector{String},String,Bool}
named_completions, partial, should_complete = try
complete_line_named(st.p.complete, st, s.active_module; hint = true)
catch
@debug "error completing line for hint" exception=current_exceptions()
return clear_hint(st)
end
completions = map(x -> x.completion, named_completions)

isempty(completions) && return clear_hint(st)
# Don't complete for single chars, given e.g. `x` completes to `xor`
if length(partial) > 1 && should_complete
Expand Down Expand Up @@ -425,7 +453,7 @@ function clear_hint(s::ModeState)
end

function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false)
completions, partial, should_complete = complete_line(s.p.complete, s, mod; hint)::Tuple{Vector{String},String,Bool}
completions, partial, should_complete = complete_line_named(s.p.complete, s, mod; hint)
isempty(completions) && return false
if !should_complete
# should_complete is false for cases where we only want to show
Expand All @@ -435,7 +463,7 @@ function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=fal
# Replace word by completion
prev_pos = position(s)
push_undo(s)
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1])
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1].completion)
else
p = common_prefix(completions)
if !isempty(p) && p != partial
Expand Down
6 changes: 3 additions & 3 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -843,22 +843,22 @@ function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; h
full = LineEdit.input_string(s)
ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
c.modifiers = LineEdit.Modifiers()
return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
end

function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
# First parse everything up to the current position
partial = beforecursor(s.input_buffer)
full = LineEdit.input_string(s)
ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
end

function complete_line(c::LatexCompletions, s; hint::Bool=false)
partial = beforecursor(LineEdit.buffer(s))
full = LineEdit.input_string(s)::String
ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
end

with_repl_linfo(f, repl) = f(outstream(repl))
Expand Down
34 changes: 22 additions & 12 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module REPLCompletions

export completions, shell_completions, bslash_completions, completion_text
export completions, shell_completions, bslash_completions, completion_text, named_completion

using Core: Const
# We want to insulate the REPLCompletion module from any changes the user may
Expand All @@ -13,6 +13,8 @@ using Base.Meta
using Base: propertynames, something, IdSet
using Base.Filesystem: _readdirx

using ..REPL.LineEdit: NamedCompletion

abstract type Completion end

struct TextCompletion <: Completion
Expand Down Expand Up @@ -57,8 +59,10 @@ struct MethodCompletion <: Completion
end

struct BslashCompletion <: Completion
bslash::String
completion::String # what is actually completed, for example "\trianglecdot"
name::String # what is displayed, for example "◬ \trianglecdot"
end
BslashCompletion(completion::String) = BslashCompletion(completion, completion)

struct ShellCompletion <: Completion
text::String
Expand Down Expand Up @@ -114,13 +118,21 @@ _completion_text(c::PackageCompletion) = c.package
_completion_text(c::PropertyCompletion) = sprint(Base.show_sym, c.property)
_completion_text(c::FieldCompletion) = sprint(Base.show_sym, c.field)
_completion_text(c::MethodCompletion) = repr(c.method)
_completion_text(c::BslashCompletion) = c.bslash
_completion_text(c::ShellCompletion) = c.text
_completion_text(c::DictCompletion) = c.key
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='

completion_text(c) = _completion_text(c)::String

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

function named_completion(c)
text = completion_text(c)::String
return NamedCompletion(text, text)
end

named_completion_completion(c) = named_completion(c).completion::String

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

function completes_global(x, name)
Expand Down Expand Up @@ -984,12 +996,10 @@ function bslash_completions(string::String, pos::Int, hint::Bool=false)
end
# return possible matches; these cannot be mixed with regular
# Julian completions as only latex / emoji symbols contain the leading \
if startswith(s, "\\:") # emoji
namelist = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols))
else # latex
namelist = Iterators.filter(k -> startswith(k, s), keys(latex_symbols))
end
return (true, (Completion[BslashCompletion(name) for name in sort!(collect(namelist))], slashpos:pos, true))
symbol_dict = startswith(s, "\\:") ? emoji_symbols : latex_symbols
namelist = Iterators.filter(k -> startswith(k, s), keys(symbol_dict))
completions = Completion[BslashCompletion(name, "$(symbol_dict[name]) $name") for name in sort!(collect(namelist))]
return (true, (completions, slashpos:pos, true))
end
return (false, (Completion[], 0:-1, false))
end
Expand Down Expand Up @@ -1099,7 +1109,7 @@ function complete_keyword_argument(partial::String, last_idx::Int, context_modul
complete_keyval!(suggestions, last_word)
end

return sort!(suggestions, by=completion_text), wordrange
return sort!(suggestions, by=named_completion_completion), wordrange
end

function get_loading_candidates(pkgstarts::String, project_file::String)
Expand Down Expand Up @@ -1298,7 +1308,7 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
complete_identifiers!(suggestions, context_module, string, name,
pos, separatorpos, startpos;
shift)
return sort!(unique!(completion_text, suggestions), by=completion_text), (separatorpos+1):pos, true
return sort!(unique!(named_completion, suggestions), by=named_completion_completion), (separatorpos+1):pos, true
elseif inc_tag === :cmd
# TODO: should this call shell_completions instead of partially reimplementing it?
let m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial)) # fuzzy shell_parse in reverse
Expand Down Expand Up @@ -1496,7 +1506,7 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
complete_identifiers!(suggestions, context_module, string, name,
pos, separatorpos, startpos;
comp_keywords, complete_modules_only, shift)
return sort!(unique!(completion_text, suggestions), by=completion_text), namepos:pos, true
return sort!(unique!(named_completion, suggestions), by=named_completion_completion), namepos:pos, true
end

function shell_completions(string, pos, hint::Bool=false)
Expand Down
33 changes: 23 additions & 10 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,23 @@ end

function map_completion_text(completions)
c, r, res = completions
return map(completion_text, c), r, res
return map(x -> named_completion(x).completion, c), r, res
end

function map_named_completion(completions)
c, r, res = completions
return map(named_completion, c), r, res
end

test_complete(s) = map_completion_text(@inferred(completions(s, lastindex(s))))
test_scomplete(s) = map_completion_text(@inferred(shell_completions(s, lastindex(s))))
test_bslashcomplete(s) = map_completion_text(@inferred(bslash_completions(s, lastindex(s)))[2])
test_complete_context(s, m=@__MODULE__; shift::Bool=true) =
map_completion_text(@inferred(completions(s,lastindex(s), m, shift)))
test_complete_foo(s) = test_complete_context(s, Main.CompletionFoo)
test_complete_noshift(s) = map_completion_text(@inferred(completions(s, lastindex(s), Main, false)))

test_bslashcomplete(s) = map_named_completion(@inferred(bslash_completions(s, lastindex(s)))[2])

test_methods_list(@nospecialize(f), tt) = map(x -> string(x.method), Base._methods_by_ftype(Base.signature_type(f, tt), 10, Base.get_world_counter()))


Expand Down Expand Up @@ -350,36 +356,43 @@ end
# test latex symbol completions
let s = "\\alpha"
c, r = test_bslashcomplete(s)
@test c[1] == "α"
@test c[1].completion == "α"
@test c[1].name == "α"
@test r == 1:lastindex(s)
@test length(c) == 1
end

# test latex symbol completions after unicode #9209
let s = "α\\alpha"
c, r = test_bslashcomplete(s)
@test c[1] == "α"
@test c[1].completion == "α"
@test c[1].name == "α"
@test r == 3:sizeof(s)
@test length(c) == 1
end

# test emoji symbol completions
let s = "\\:koala:"
c, r = test_bslashcomplete(s)
@test c[1] == "🐨"
@test c[1].completion == "🐨"
@test c[1].name == "🐨"
@test r == 1:sizeof(s)
@test length(c) == 1
end

let s = "\\:ko"
c, r = test_bslashcomplete(s)
@test "\\:koala:" in c
ko = only(filter(c) do namedcompletion
namedcompletion.completion == "\\:koala:"
end)
@test ko.name == "🐨 \\:koala:"
end

# test emoji symbol completions after unicode #9209
let s = "α\\:koala:"
c, r = test_bslashcomplete(s)
@test c[1] == "🐨"
@test c[1].name == "🐨"
@test c[1].completion == "🐨"
@test r == 3:sizeof(s)
@test length(c) == 1
end
Expand Down Expand Up @@ -1069,8 +1082,8 @@ let s, c, r
# Issue #8047
s = "@show \"/dev/nul\""
c,r = completions(s, 15)
c = map(completion_text, c)
@test "null\"" in c
c = map(named_completion, c)
@test "null\"" in [_c.completion for _c in c]
@test r == 13:15
@test s[r] == "nul"

Expand Down Expand Up @@ -1476,7 +1489,7 @@ function test_dict_completion(dict_name)
@test c == Any["\"abcd\"]"]
s = "$dict_name[\"abcd]" # trailing close bracket
c, r = completions(s, lastindex(s) - 1)
c = map(completion_text, c)
c = map(x -> named_completion(x).completion, c)
@test c == Any["\"abcd\""]
s = "$dict_name[:b"
c, r = test_complete(s)
Expand Down

2 comments on commit 9acf112

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

Please sign in to comment.