Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Standard library changes

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

Expand Down
22 changes: 22 additions & 0 deletions stdlib/REPL/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,28 @@ atreplinit(customize_keys)

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

### Automatic bracket insertion

The Julia REPL supports automatically inserting closing brackets, parentheses, braces, and quotes
when you type the opening character.

When enabled, typing an opening bracket `(`, `{`, or `[` will automatically insert the matching
closing bracket `)`, `}`, or `]` and position the cursor between them. The same behavior applies
to quotes (`"`, `'`, and `` ` ``). If you then type the closing character, the REPL will skip over
the auto-inserted character instead of inserting a duplicate. Additionally, pressing backspace
immediately after auto-insertion will remove both the opening and closing characters.

To disable this feature, add the following to your `~/.julia/config/startup.jl` file:

```julia
atreplinit() do repl
# Robust against older julia versions
if hasfield(typeof(repl.options), :auto_insert_closing_bracket)
repl.options.auto_insert_closing_bracket = false
end
end
```

## Tab completion

In the Julian, pkg and help modes of the REPL, one can enter the first few characters of a function
Expand Down
136 changes: 136 additions & 0 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,142 @@ const escape_defaults = merge!(
AnyDict("\e[$(c)l" => nothing for c in 1:20)
)


# Keymap for automatic bracket/quote insertion and completion
const bracket_insert_keymap = AnyDict()
let
# Determine when we should not close a bracket/quote
function should_skip_closing_bracket(left_peek, v)
# Don't close if we already have an open quote immediately before (triple quote case)
# For quotes, also check for transpose expressions: issue JuliaLang/OhMyREPL.jl#200
left_peek == v && return true
if v == '\''
tr_expr = isletter(left_peek) || isnumeric(left_peek) || left_peek == '_' || left_peek == ']'
return tr_expr
end
return false
end

function peek_char_left(b::IOBuffer)
p = position(b)
c = char_move_left(b)
seek(b, p)
return c
end

# Check if there's an unmatched opening quote before the cursor
function has_unmatched_quote(buf::IOBuffer, quote_char::Char)
pos = position(buf)
content = String(buf.data[1:pos])
isempty(content) && return false

# Count unescaped quotes before cursor position
count = 0
i = 1
while i <= length(content)
if content[i] == quote_char
# Check if escaped by counting preceding backslashes
num_backslashes = 0
j = i - 1
while j >= 1 && content[j] == '\\'
num_backslashes += 1
j -= 1
end
# If even number of backslashes (including zero), the quote is not escaped
if num_backslashes % 2 == 0
count += 1
end
end
i = nextind(content, i)
end
return isodd(count)
end

# Left/right bracket pairs
bracket_pairs = (('(', ')'), ('{', '}'), ('[', ']'))
right_brackets_ws = (')', '}', ']', ' ', '\t', '\n')

for (left, right) in bracket_pairs
# Left bracket: insert both and move cursor between them
bracket_insert_keymap[left] = (s::MIState, o...) -> begin
buf = buffer(s)
edit_insert(buf, left)
if eof(buf) || peek(buf, Char) in right_brackets_ws
edit_insert(buf, right)
edit_move_left(buf)
end
refresh_line(s)
end

# Right bracket: skip over if next char matches, otherwise insert
bracket_insert_keymap[right] = (s::MIState, o...) -> begin
buf = buffer(s)
if !eof(buf) && peek(buf, Char) == right
edit_move_right(buf)
else
edit_insert(buf, right)
end
refresh_line(s)
end
end

# Quote characters (need special handling for transpose detection)
for quote_char in ('"', '\'', '`')
bracket_insert_keymap[quote_char] = (s::MIState, o...) -> begin
buf = buffer(s)
if !eof(buf) && peek(buf, Char) == quote_char
# Skip over closing quote
edit_move_right(buf)
elseif position(buf) > 0 && should_skip_closing_bracket(peek_char_left(buf), quote_char)
# Don't auto-close (e.g., for transpose or triple quotes)
edit_insert(buf, quote_char)
elseif quote_char in ('"', '\'', '`') && has_unmatched_quote(buf, quote_char)
# For quotes, check if we're closing an existing string
edit_insert(buf, quote_char)
else
# Insert both quotes
edit_insert(buf, quote_char)
edit_insert(buf, quote_char)
edit_move_left(buf)
end
refresh_line(s)
end
end

# Backspace - also remove matching closing bracket/quote
bracket_insert_keymap['\b'] = (s::MIState, o...) -> begin
if is_region_active(s)
return edit_kill_region(s)
elseif isempty(s) || position(buffer(s)) == 0
# Handle transitioning to main mode
repl = Base.active_repl
mirepl = isdefined(repl, :mi) ? repl.mi : repl
main_mode = mirepl.interface.modes[1]
buf = copy(buffer(s))
transition(s, main_mode) do
state(s, main_mode).input_buffer = buf
end
return
end

buf = buffer(s)
left_brackets = ('(', '{', '[', '"', '\'', '`')
right_brackets = (')', '}', ']', '"', '\'', '`')

if !eof(buf) && position(buf) > 0
left_char = peek_char_left(buf)
i = findfirst(isequal(left_char), left_brackets)
if i !== nothing && peek(buf, Char) == right_brackets[i]
# Remove both the left and right bracket/quote
edit_delete(buf)
edit_backspace(buf)
return refresh_line(s)
end
end
return edit_backspace(s)
end
end

mutable struct HistoryPrompt <: TextInterface
hp::HistoryProvider
complete::CompletionProvider
Expand Down
31 changes: 28 additions & 3 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,18 @@ function setup_interface(
end
Base.errormonitor(t_replswitch)
else
edit_insert(s, ']')
# Use bracket insertion if enabled, otherwise just insert
if repl.options.auto_insert_closing_bracket
buf = LineEdit.buffer(s)
if !eof(buf) && LineEdit.peek(buf, Char) == ']'
LineEdit.edit_move_right(buf)
else
edit_insert(buf, ']')
end
LineEdit.refresh_line(s)
else
edit_insert(s, ']')
end
LineEdit.check_show_hint(s)
end
end,
Expand Down Expand Up @@ -1671,14 +1682,28 @@ function setup_interface(

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

a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
# Build keymap list - add bracket insertion if enabled
base_keymaps = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap]
if repl.options.auto_insert_closing_bracket
push!(base_keymaps, LineEdit.bracket_insert_keymap)
end
push!(base_keymaps, LineEdit.default_keymap, LineEdit.escape_defaults)

a = base_keymaps
prepend!(a, extra_repl_keymap)

julia_prompt.keymap_dict = LineEdit.keymap(a)

mk = mode_keymap(julia_prompt)

b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
# Build keymap list for other modes
mode_base_keymaps = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap]
if repl.options.auto_insert_closing_bracket
push!(mode_base_keymaps, LineEdit.bracket_insert_keymap)
end
push!(mode_base_keymaps, LineEdit.default_keymap, LineEdit.escape_defaults)

b = mode_base_keymaps
prepend!(b, extra_repl_keymap)

shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
Expand Down
4 changes: 3 additions & 1 deletion stdlib/REPL/src/options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mutable struct Options
# refresh after time delay
auto_refresh_time_delay::Float64
hint_tab_completes::Bool
auto_insert_closing_bracket::Bool # automatically insert closing brackets, quotes, etc.
style_input::Bool # enable syntax highlighting for input
# default IOContext settings at the REPL
iocontext::Dict{Symbol,Any}
Expand All @@ -50,6 +51,7 @@ Options(;
auto_indent_time_threshold = 0.005,
auto_refresh_time_delay = 0.0, # this no longer seems beneficial
hint_tab_completes = true,
auto_insert_closing_bracket = true,
style_input = true,
iocontext = Dict{Symbol,Any}()) =
Options(hascolor, extra_keymap, tabwidth,
Expand All @@ -59,7 +61,7 @@ Options(;
backspace_align, backspace_adjust, confirm_exit,
auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste,
auto_indent_time_threshold, auto_refresh_time_delay,
hint_tab_completes, style_input,
hint_tab_completes, auto_insert_closing_bracket, style_input,
iocontext)

# for use by REPLs not having an options field
Expand Down
Loading