Skip to content

Commit 810a1bd

Browse files
authored
add support for automatic bracket completion in REPL input (#59847)
1 parent 03d11ac commit 810a1bd

File tree

7 files changed

+379
-9
lines changed

7 files changed

+379
-9
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Standard library changes
9898

9999
* The Julia REPL now support bracketed paste on Windows which should significantly speed up pasting large code blocks into the REPL ([#59825])
100100
* The REPL now provides syntax highlighting for input as you type. See the REPL docs for more info about customization.
101+
* The REPL now supports automatic insertion of closing brackets, parentheses, and quotes. See the REPL docs for more info about customization.
101102
* The display of `AbstractChar`s in the main REPL mode now includes LaTeX input information like what is shown in help mode ([#58181]).
102103
* Display of repeated frames and cycles in stack traces has been improved by bracketing them in the trace and treating them consistently ([#55841]).
103104

stdlib/REPL/docs/src/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,28 @@ atreplinit(customize_keys)
317317

318318
Users should refer to `LineEdit.jl` to discover the available actions on key input.
319319

320+
### Automatic bracket insertion
321+
322+
The Julia REPL supports automatically inserting closing brackets, parentheses, braces, and quotes
323+
when you type the opening character.
324+
325+
When enabled, typing an opening bracket `(`, `{`, or `[` will automatically insert the matching
326+
closing bracket `)`, `}`, or `]` and position the cursor between them. The same behavior applies
327+
to quotes (`"`, `'`, and `` ` ``). If you then type the closing character, the REPL will skip over
328+
the auto-inserted character instead of inserting a duplicate. Additionally, pressing backspace
329+
immediately after auto-insertion will remove both the opening and closing characters.
330+
331+
To disable this feature, add the following to your `~/.julia/config/startup.jl` file:
332+
333+
```julia
334+
atreplinit() do repl
335+
# Robust against older julia versions
336+
if hasfield(typeof(repl.options), :auto_insert_closing_bracket)
337+
repl.options.auto_insert_closing_bracket = false
338+
end
339+
end
340+
```
341+
320342
## Tab completion
321343

322344
In the Julian, pkg and help modes of the REPL, one can enter the first few characters of a function

stdlib/REPL/src/LineEdit.jl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,142 @@ const escape_defaults = merge!(
21012101
AnyDict("\e[$(c)l" => nothing for c in 1:20)
21022102
)
21032103

2104+
2105+
# Keymap for automatic bracket/quote insertion and completion
2106+
const bracket_insert_keymap = AnyDict()
2107+
let
2108+
# Determine when we should not close a bracket/quote
2109+
function should_skip_closing_bracket(left_peek, v)
2110+
# Don't close if we already have an open quote immediately before (triple quote case)
2111+
# For quotes, also check for transpose expressions: issue JuliaLang/OhMyREPL.jl#200
2112+
left_peek == v && return true
2113+
if v == '\''
2114+
tr_expr = isletter(left_peek) || isnumeric(left_peek) || left_peek == '_' || left_peek == ']'
2115+
return tr_expr
2116+
end
2117+
return false
2118+
end
2119+
2120+
function peek_char_left(b::IOBuffer)
2121+
p = position(b)
2122+
c = char_move_left(b)
2123+
seek(b, p)
2124+
return c
2125+
end
2126+
2127+
# Check if there's an unmatched opening quote before the cursor
2128+
function has_unmatched_quote(buf::IOBuffer, quote_char::Char)
2129+
pos = position(buf)
2130+
content = String(buf.data[1:pos])
2131+
isempty(content) && return false
2132+
2133+
# Count unescaped quotes before cursor position
2134+
count = 0
2135+
i = 1
2136+
while i <= length(content)
2137+
if content[i] == quote_char
2138+
# Check if escaped by counting preceding backslashes
2139+
num_backslashes = 0
2140+
j = i - 1
2141+
while j >= 1 && content[j] == '\\'
2142+
num_backslashes += 1
2143+
j -= 1
2144+
end
2145+
# If even number of backslashes (including zero), the quote is not escaped
2146+
if num_backslashes % 2 == 0
2147+
count += 1
2148+
end
2149+
end
2150+
i = nextind(content, i)
2151+
end
2152+
return isodd(count)
2153+
end
2154+
2155+
# Left/right bracket pairs
2156+
bracket_pairs = (('(', ')'), ('{', '}'), ('[', ']'))
2157+
right_brackets_ws = (')', '}', ']', ' ', '\t', '\n')
2158+
2159+
for (left, right) in bracket_pairs
2160+
# Left bracket: insert both and move cursor between them
2161+
bracket_insert_keymap[left] = (s::MIState, o...) -> begin
2162+
buf = buffer(s)
2163+
edit_insert(buf, left)
2164+
if eof(buf) || peek(buf, Char) in right_brackets_ws
2165+
edit_insert(buf, right)
2166+
edit_move_left(buf)
2167+
end
2168+
refresh_line(s)
2169+
end
2170+
2171+
# Right bracket: skip over if next char matches, otherwise insert
2172+
bracket_insert_keymap[right] = (s::MIState, o...) -> begin
2173+
buf = buffer(s)
2174+
if !eof(buf) && peek(buf, Char) == right
2175+
edit_move_right(buf)
2176+
else
2177+
edit_insert(buf, right)
2178+
end
2179+
refresh_line(s)
2180+
end
2181+
end
2182+
2183+
# Quote characters (need special handling for transpose detection)
2184+
for quote_char in ('"', '\'', '`')
2185+
bracket_insert_keymap[quote_char] = (s::MIState, o...) -> begin
2186+
buf = buffer(s)
2187+
if !eof(buf) && peek(buf, Char) == quote_char
2188+
# Skip over closing quote
2189+
edit_move_right(buf)
2190+
elseif position(buf) > 0 && should_skip_closing_bracket(peek_char_left(buf), quote_char)
2191+
# Don't auto-close (e.g., for transpose or triple quotes)
2192+
edit_insert(buf, quote_char)
2193+
elseif quote_char in ('"', '\'', '`') && has_unmatched_quote(buf, quote_char)
2194+
# For quotes, check if we're closing an existing string
2195+
edit_insert(buf, quote_char)
2196+
else
2197+
# Insert both quotes
2198+
edit_insert(buf, quote_char)
2199+
edit_insert(buf, quote_char)
2200+
edit_move_left(buf)
2201+
end
2202+
refresh_line(s)
2203+
end
2204+
end
2205+
2206+
# Backspace - also remove matching closing bracket/quote
2207+
bracket_insert_keymap['\b'] = (s::MIState, o...) -> begin
2208+
if is_region_active(s)
2209+
return edit_kill_region(s)
2210+
elseif isempty(s) || position(buffer(s)) == 0
2211+
# Handle transitioning to main mode
2212+
repl = Base.active_repl
2213+
mirepl = isdefined(repl, :mi) ? repl.mi : repl
2214+
main_mode = mirepl.interface.modes[1]
2215+
buf = copy(buffer(s))
2216+
transition(s, main_mode) do
2217+
state(s, main_mode).input_buffer = buf
2218+
end
2219+
return
2220+
end
2221+
2222+
buf = buffer(s)
2223+
left_brackets = ('(', '{', '[', '"', '\'', '`')
2224+
right_brackets = (')', '}', ']', '"', '\'', '`')
2225+
2226+
if !eof(buf) && position(buf) > 0
2227+
left_char = peek_char_left(buf)
2228+
i = findfirst(isequal(left_char), left_brackets)
2229+
if i !== nothing && peek(buf, Char) == right_brackets[i]
2230+
# Remove both the left and right bracket/quote
2231+
edit_delete(buf)
2232+
edit_backspace(buf)
2233+
return refresh_line(s)
2234+
end
2235+
end
2236+
return edit_backspace(s)
2237+
end
2238+
end
2239+
21042240
mutable struct HistoryPrompt <: TextInterface
21052241
hp::HistoryProvider
21062242
complete::CompletionProvider

stdlib/REPL/src/REPL.jl

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,18 @@ function setup_interface(
14991499
end
15001500
Base.errormonitor(t_replswitch)
15011501
else
1502-
edit_insert(s, ']')
1502+
# Use bracket insertion if enabled, otherwise just insert
1503+
if repl.options.auto_insert_closing_bracket
1504+
buf = LineEdit.buffer(s)
1505+
if !eof(buf) && LineEdit.peek(buf, Char) == ']'
1506+
LineEdit.edit_move_right(buf)
1507+
else
1508+
edit_insert(buf, ']')
1509+
end
1510+
LineEdit.refresh_line(s)
1511+
else
1512+
edit_insert(s, ']')
1513+
end
15031514
LineEdit.check_show_hint(s)
15041515
end
15051516
end,
@@ -1671,14 +1682,28 @@ function setup_interface(
16711682

16721683
prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
16731684

1674-
a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1685+
# Build keymap list - add bracket insertion if enabled
1686+
base_keymaps = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap]
1687+
if repl.options.auto_insert_closing_bracket
1688+
push!(base_keymaps, LineEdit.bracket_insert_keymap)
1689+
end
1690+
push!(base_keymaps, LineEdit.default_keymap, LineEdit.escape_defaults)
1691+
1692+
a = base_keymaps
16751693
prepend!(a, extra_repl_keymap)
16761694

16771695
julia_prompt.keymap_dict = LineEdit.keymap(a)
16781696

16791697
mk = mode_keymap(julia_prompt)
16801698

1681-
b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1699+
# Build keymap list for other modes
1700+
mode_base_keymaps = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap]
1701+
if repl.options.auto_insert_closing_bracket
1702+
push!(mode_base_keymaps, LineEdit.bracket_insert_keymap)
1703+
end
1704+
push!(mode_base_keymaps, LineEdit.default_keymap, LineEdit.escape_defaults)
1705+
1706+
b = mode_base_keymaps
16821707
prepend!(b, extra_repl_keymap)
16831708

16841709
shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)

stdlib/REPL/src/options.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mutable struct Options
2828
# refresh after time delay
2929
auto_refresh_time_delay::Float64
3030
hint_tab_completes::Bool
31+
auto_insert_closing_bracket::Bool # automatically insert closing brackets, quotes, etc.
3132
style_input::Bool # enable syntax highlighting for input
3233
# default IOContext settings at the REPL
3334
iocontext::Dict{Symbol,Any}
@@ -50,6 +51,7 @@ Options(;
5051
auto_indent_time_threshold = 0.005,
5152
auto_refresh_time_delay = 0.0, # this no longer seems beneficial
5253
hint_tab_completes = true,
54+
auto_insert_closing_bracket = true,
5355
style_input = true,
5456
iocontext = Dict{Symbol,Any}()) =
5557
Options(hascolor, extra_keymap, tabwidth,
@@ -59,7 +61,7 @@ Options(;
5961
backspace_align, backspace_adjust, confirm_exit,
6062
auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste,
6163
auto_indent_time_threshold, auto_refresh_time_delay,
62-
hint_tab_completes, style_input,
64+
hint_tab_completes, auto_insert_closing_bracket, style_input,
6365
iocontext)
6466

6567
# for use by REPLs not having an options field

0 commit comments

Comments
 (0)