Skip to content

Commit 9acf112

Browse files
Actually show glyphs for latex or emoji shortcodes being suggested in 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]>
1 parent 2c87290 commit 9acf112

File tree

4 files changed

+81
-30
lines changed

4 files changed

+81
-30
lines changed

stdlib/REPL/src/LineEdit.jl

+33-5
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,18 @@ struct EmptyHistoryProvider <: HistoryProvider end
181181

182182
reset_state(::EmptyHistoryProvider) = nothing
183183

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

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

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

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

345+
show_completions(s::PromptState, completions::Vector{NamedCompletion}) = show_completions(s, map(x -> x.name, completions))
346+
333347
# Show available completions
334348
function show_completions(s::PromptState, completions::Vector{String})
335349
# skip any lines of input after the cursor
@@ -374,6 +388,18 @@ function complete_line(s::MIState)
374388
end
375389
end
376390

391+
# due to close coupling of the Pkg ReplExt `complete_line` can still return a vector of strings,
392+
# so we convert those in this helper
393+
function complete_line_named(args...; kwargs...)::Tuple{Vector{NamedCompletion},String,Bool}
394+
result = complete_line(args...; kwargs...)::Union{Tuple{Vector{NamedCompletion},String,Bool},Tuple{Vector{String},String,Bool}}
395+
if result isa Tuple{Vector{NamedCompletion},String,Bool}
396+
return result
397+
else
398+
completions, partial, should_complete = result
399+
return map(NamedCompletion, completions), partial, should_complete
400+
end
401+
end
402+
377403
function check_for_hint(s::MIState)
378404
st = state(s)
379405
if !options(st).hint_tab_completes || !eof(buffer(st))
@@ -383,12 +409,14 @@ function check_for_hint(s::MIState)
383409
return clear_hint(st)
384410
end
385411

386-
completions, partial, should_complete = try
387-
complete_line(st.p.complete, st, s.active_module; hint = true)::Tuple{Vector{String},String,Bool}
412+
named_completions, partial, should_complete = try
413+
complete_line_named(st.p.complete, st, s.active_module; hint = true)
388414
catch
389415
@debug "error completing line for hint" exception=current_exceptions()
390416
return clear_hint(st)
391417
end
418+
completions = map(x -> x.completion, named_completions)
419+
392420
isempty(completions) && return clear_hint(st)
393421
# Don't complete for single chars, given e.g. `x` completes to `xor`
394422
if length(partial) > 1 && should_complete
@@ -425,7 +453,7 @@ function clear_hint(s::ModeState)
425453
end
426454

427455
function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false)
428-
completions, partial, should_complete = complete_line(s.p.complete, s, mod; hint)::Tuple{Vector{String},String,Bool}
456+
completions, partial, should_complete = complete_line_named(s.p.complete, s, mod; hint)
429457
isempty(completions) && return false
430458
if !should_complete
431459
# should_complete is false for cases where we only want to show
@@ -435,7 +463,7 @@ function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=fal
435463
# Replace word by completion
436464
prev_pos = position(s)
437465
push_undo(s)
438-
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1])
466+
edit_splice!(s, (prev_pos - sizeof(partial)) => prev_pos, completions[1].completion)
439467
else
440468
p = common_prefix(completions)
441469
if !isempty(p) && p != partial

stdlib/REPL/src/REPL.jl

+3-3
Original file line numberDiff line numberDiff line change
@@ -843,22 +843,22 @@ function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; h
843843
full = LineEdit.input_string(s)
844844
ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
845845
c.modifiers = LineEdit.Modifiers()
846-
return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
846+
return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
847847
end
848848

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

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

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

stdlib/REPL/src/REPLCompletions.jl

+22-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module REPLCompletions
44

5-
export completions, shell_completions, bslash_completions, completion_text
5+
export completions, shell_completions, bslash_completions, completion_text, named_completion
66

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

16+
using ..REPL.LineEdit: NamedCompletion
17+
1618
abstract type Completion end
1719

1820
struct TextCompletion <: Completion
@@ -57,8 +59,10 @@ struct MethodCompletion <: Completion
5759
end
5860

5961
struct BslashCompletion <: Completion
60-
bslash::String
62+
completion::String # what is actually completed, for example "\trianglecdot"
63+
name::String # what is displayed, for example "◬ \trianglecdot"
6164
end
65+
BslashCompletion(completion::String) = BslashCompletion(completion, completion)
6266

6367
struct ShellCompletion <: Completion
6468
text::String
@@ -114,13 +118,21 @@ _completion_text(c::PackageCompletion) = c.package
114118
_completion_text(c::PropertyCompletion) = sprint(Base.show_sym, c.property)
115119
_completion_text(c::FieldCompletion) = sprint(Base.show_sym, c.field)
116120
_completion_text(c::MethodCompletion) = repr(c.method)
117-
_completion_text(c::BslashCompletion) = c.bslash
118121
_completion_text(c::ShellCompletion) = c.text
119122
_completion_text(c::DictCompletion) = c.key
120123
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='
121124

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

127+
named_completion(c::BslashCompletion) = NamedCompletion(c.completion, c.name)
128+
129+
function named_completion(c)
130+
text = completion_text(c)::String
131+
return NamedCompletion(text, text)
132+
end
133+
134+
named_completion_completion(c) = named_completion(c).completion::String
135+
124136
const Completions = Tuple{Vector{Completion}, UnitRange{Int}, Bool}
125137

126138
function completes_global(x, name)
@@ -984,12 +996,10 @@ function bslash_completions(string::String, pos::Int, hint::Bool=false)
984996
end
985997
# return possible matches; these cannot be mixed with regular
986998
# Julian completions as only latex / emoji symbols contain the leading \
987-
if startswith(s, "\\:") # emoji
988-
namelist = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols))
989-
else # latex
990-
namelist = Iterators.filter(k -> startswith(k, s), keys(latex_symbols))
991-
end
992-
return (true, (Completion[BslashCompletion(name) for name in sort!(collect(namelist))], slashpos:pos, true))
999+
symbol_dict = startswith(s, "\\:") ? emoji_symbols : latex_symbols
1000+
namelist = Iterators.filter(k -> startswith(k, s), keys(symbol_dict))
1001+
completions = Completion[BslashCompletion(name, "$(symbol_dict[name]) $name") for name in sort!(collect(namelist))]
1002+
return (true, (completions, slashpos:pos, true))
9931003
end
9941004
return (false, (Completion[], 0:-1, false))
9951005
end
@@ -1099,7 +1109,7 @@ function complete_keyword_argument(partial::String, last_idx::Int, context_modul
10991109
complete_keyval!(suggestions, last_word)
11001110
end
11011111

1102-
return sort!(suggestions, by=completion_text), wordrange
1112+
return sort!(suggestions, by=named_completion_completion), wordrange
11031113
end
11041114

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

15021512
function shell_completions(string, pos, hint::Bool=false)

stdlib/REPL/test/replcompletions.jl

+23-10
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,23 @@ end
170170

171171
function map_completion_text(completions)
172172
c, r, res = completions
173-
return map(completion_text, c), r, res
173+
return map(x -> named_completion(x).completion, c), r, res
174+
end
175+
176+
function map_named_completion(completions)
177+
c, r, res = completions
178+
return map(named_completion, c), r, res
174179
end
175180

176181
test_complete(s) = map_completion_text(@inferred(completions(s, lastindex(s))))
177182
test_scomplete(s) = map_completion_text(@inferred(shell_completions(s, lastindex(s))))
178-
test_bslashcomplete(s) = map_completion_text(@inferred(bslash_completions(s, lastindex(s)))[2])
179183
test_complete_context(s, m=@__MODULE__; shift::Bool=true) =
180184
map_completion_text(@inferred(completions(s,lastindex(s), m, shift)))
181185
test_complete_foo(s) = test_complete_context(s, Main.CompletionFoo)
182186
test_complete_noshift(s) = map_completion_text(@inferred(completions(s, lastindex(s), Main, false)))
183187

188+
test_bslashcomplete(s) = map_named_completion(@inferred(bslash_completions(s, lastindex(s)))[2])
189+
184190
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()))
185191

186192

@@ -350,36 +356,43 @@ end
350356
# test latex symbol completions
351357
let s = "\\alpha"
352358
c, r = test_bslashcomplete(s)
353-
@test c[1] == "α"
359+
@test c[1].completion == "α"
360+
@test c[1].name == "α"
354361
@test r == 1:lastindex(s)
355362
@test length(c) == 1
356363
end
357364

358365
# test latex symbol completions after unicode #9209
359366
let s = "α\\alpha"
360367
c, r = test_bslashcomplete(s)
361-
@test c[1] == "α"
368+
@test c[1].completion == "α"
369+
@test c[1].name == "α"
362370
@test r == 3:sizeof(s)
363371
@test length(c) == 1
364372
end
365373

366374
# test emoji symbol completions
367375
let s = "\\:koala:"
368376
c, r = test_bslashcomplete(s)
369-
@test c[1] == "🐨"
377+
@test c[1].completion == "🐨"
378+
@test c[1].name == "🐨"
370379
@test r == 1:sizeof(s)
371380
@test length(c) == 1
372381
end
373382

374383
let s = "\\:ko"
375384
c, r = test_bslashcomplete(s)
376-
@test "\\:koala:" in c
385+
ko = only(filter(c) do namedcompletion
386+
namedcompletion.completion == "\\:koala:"
387+
end)
388+
@test ko.name == "🐨 \\:koala:"
377389
end
378390

379391
# test emoji symbol completions after unicode #9209
380392
let s = "α\\:koala:"
381393
c, r = test_bslashcomplete(s)
382-
@test c[1] == "🐨"
394+
@test c[1].name == "🐨"
395+
@test c[1].completion == "🐨"
383396
@test r == 3:sizeof(s)
384397
@test length(c) == 1
385398
end
@@ -1069,8 +1082,8 @@ let s, c, r
10691082
# Issue #8047
10701083
s = "@show \"/dev/nul\""
10711084
c,r = completions(s, 15)
1072-
c = map(completion_text, c)
1073-
@test "null\"" in c
1085+
c = map(named_completion, c)
1086+
@test "null\"" in [_c.completion for _c in c]
10741087
@test r == 13:15
10751088
@test s[r] == "nul"
10761089

@@ -1476,7 +1489,7 @@ function test_dict_completion(dict_name)
14761489
@test c == Any["\"abcd\"]"]
14771490
s = "$dict_name[\"abcd]" # trailing close bracket
14781491
c, r = completions(s, lastindex(s) - 1)
1479-
c = map(completion_text, c)
1492+
c = map(x -> named_completion(x).completion, c)
14801493
@test c == Any["\"abcd\""]
14811494
s = "$dict_name[:b"
14821495
c, r = test_complete(s)

0 commit comments

Comments
 (0)