Skip to content

Commit 786e304

Browse files
authored
add syntax highlighting to the REPL input (#59778)
1 parent c171ddb commit 786e304

File tree

8 files changed

+551
-27
lines changed

8 files changed

+551
-27
lines changed

NEWS.md

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

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

contrib/generate_precompile.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ precompile(Tuple{typeof(Base.Terminals.enable_bracketed_paste), Base.Terminals.T
4141
precompile(Tuple{typeof(Base.Terminals.width), Base.Terminals.TTYTerminal})
4242
precompile(Tuple{typeof(Base.Terminals.height), Base.Terminals.TTYTerminal})
4343
precompile(Tuple{typeof(Base.write), Base.Terminals.TTYTerminal, Array{UInt8, 1}})
44+
precompile(Tuple{typeof(Base.isempty), Base.AnnotatedString{String}}
4445
4546
# loading.jl - without these each precompile worker would precompile these because they're hit before pkgimages are loaded
4647
precompile(Base.__require, (Module, Symbol))

stdlib/REPL/docs/src/index.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,183 @@ mmap(file::AbstractString, ::Type{T}, len::Integer) where T<:BitArray in Mmap at
533533
mmap(file::AbstractString, ::Type{T}, len::Integer, offset::Integer; grow, shared) where T<:BitArray in Mmap at Mmap/src/Mmap.jl:322
534534
```
535535

536+
## Syntax Highlighting
537+
538+
The REPL provides syntax highlighting for input as you type.
539+
Syntax highlighting is enabled by default but can be disabled in your `~/.julia/config/startup.jl`:
540+
541+
```julia
542+
atreplinit() do repl
543+
repl.options.style_input = false
544+
end
545+
```
546+
547+
### Customizing Syntax Highlighting Colors
548+
549+
The default syntax highlighting theme is quite conservative but can be customized using a TOML file `faces.toml` (https://julialang.github.io/StyledStrings.jl/dev/#stdlib-styledstrings-face-toml) in `.julia/config` (or by explicitly loading the faces from a face toml file).
550+
551+
552+
<details>
553+
<summary>Example: Monokai color theme (click to expand)</summary>
554+
555+
```toml
556+
# Monokai color theme for Julia syntax highlighting
557+
558+
[julia_macro]
559+
foreground = "#A6E22E"
560+
561+
[julia_symbol]
562+
foreground = "#AE81FF"
563+
564+
[julia_singleton_identifier]
565+
inherit = "julia_symbol"
566+
567+
[julia_type]
568+
foreground = "#66D9EF"
569+
570+
[julia_typedec]
571+
foreground = "#66D9EF"
572+
weight = "bold"
573+
574+
[julia_comment]
575+
foreground = "#75715E"
576+
italic = true
577+
578+
[julia_string]
579+
foreground = "#E6DB74"
580+
581+
[julia_regex]
582+
inherit = "julia_string"
583+
584+
[julia_backslash_literal]
585+
foreground = "#FD971F"
586+
inherit = "julia_string"
587+
588+
[julia_string_delim]
589+
foreground = "#E6DB74"
590+
weight = "bold"
591+
592+
[julia_cmdstring]
593+
inherit = "julia_string"
594+
595+
[julia_char]
596+
inherit = "julia_string"
597+
598+
[julia_char_delim]
599+
inherit = "julia_string_delim"
600+
601+
[julia_number]
602+
foreground = "#AE81FF"
603+
604+
[julia_bool]
605+
foreground = "#AE81FF"
606+
weight = "bold"
607+
608+
[julia_funcall]
609+
foreground = "#A6E22E"
610+
611+
[julia_broadcast]
612+
foreground = "#F92672"
613+
weight = "bold"
614+
615+
[julia_builtin]
616+
foreground = "#66D9EF"
617+
weight = "bold"
618+
619+
[julia_operator]
620+
foreground = "#F92672"
621+
622+
[julia_comparator]
623+
inherit = "julia_operator"
624+
625+
[julia_assignment]
626+
foreground = "#F92672"
627+
weight = "bold"
628+
629+
[julia_keyword]
630+
foreground = "#F92672"
631+
weight = "bold"
632+
633+
[julia_parentheses]
634+
foreground = "#F8F8F2"
635+
636+
[julia_unpaired_parentheses]
637+
background = "#F92672"
638+
foreground = "#F8F8F0"
639+
weight = "bold"
640+
641+
[julia_error]
642+
background = "#F92672"
643+
foreground = "#F8F8F0"
644+
645+
[julia_rainbow_paren_1]
646+
foreground = "#A6E22E"
647+
inherit = "julia_parentheses"
648+
649+
[julia_rainbow_paren_2]
650+
foreground = "#66D9EF"
651+
inherit = "julia_parentheses"
652+
653+
[julia_rainbow_paren_3]
654+
foreground = "#FD971F"
655+
inherit = "julia_parentheses"
656+
657+
[julia_rainbow_paren_4]
658+
inherit = "julia_rainbow_paren_1"
659+
660+
[julia_rainbow_paren_5]
661+
inherit = "julia_rainbow_paren_2"
662+
663+
[julia_rainbow_paren_6]
664+
inherit = "julia_rainbow_paren_3"
665+
666+
# Rainbow brackets
667+
[julia_rainbow_bracket_1]
668+
foreground = "#AE81FF"
669+
inherit = "julia_parentheses"
670+
671+
[julia_rainbow_bracket_2]
672+
foreground = "#E6DB74"
673+
inherit = "julia_parentheses"
674+
675+
[julia_rainbow_bracket_3]
676+
inherit = "julia_rainbow_bracket_1"
677+
678+
[julia_rainbow_bracket_4]
679+
inherit = "julia_rainbow_bracket_2"
680+
681+
[julia_rainbow_bracket_5]
682+
inherit = "julia_rainbow_bracket_1"
683+
684+
[julia_rainbow_bracket_6]
685+
inherit = "julia_rainbow_bracket_2"
686+
687+
# Rainbow curlies
688+
[julia_rainbow_curly_1]
689+
foreground = "#F92672"
690+
inherit = "julia_parentheses"
691+
692+
[julia_rainbow_curly_2]
693+
foreground = "#A6E22E"
694+
inherit = "julia_parentheses"
695+
696+
[julia_rainbow_curly_3]
697+
inherit = "julia_rainbow_curly_1"
698+
699+
[julia_rainbow_curly_4]
700+
inherit = "julia_rainbow_curly_2"
701+
702+
[julia_rainbow_curly_5]
703+
inherit = "julia_rainbow_curly_1"
704+
705+
[julia_rainbow_curly_6]
706+
inherit = "julia_rainbow_curly_2"
707+
```
708+
709+
</details>
710+
711+
For a complete list of customizable faces, see the [JuliaSyntaxHighlighting package documentation](https://julialang.github.io/JuliaSyntaxHighlighting.jl/dev/).
712+
536713
## Customizing Colors
537714

538715
The colors used by Julia and the REPL can be customized, as well. To change the

stdlib/REPL/src/LineEdit.jl

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ module LineEdit
44

55
import ..REPL
66
using ..REPL: AbstractREPL, Options
7+
using ..REPL.StylingPasses: StylingPass, SyntaxHighlightPass, RegionHighlightPass, EnclosingParenHighlightPass, StylingContext, apply_styling_passes, merge_annotations
78

89
using ..Terminals
910
import ..Terminals: raw!, width, height, clear_line, beep
1011

12+
using StyledStrings
13+
1114
import Base: ensureroom, show, AnyDict, position
1215
using Base: something
1316

@@ -58,6 +61,7 @@ mutable struct Prompt <: TextInterface
5861
on_done::Function
5962
hist::HistoryProvider # TODO?: rename this `hp` (consistency with other TextInterfaces), or is the type-assert useful for mode(s)?
6063
sticky::Bool
64+
styling_passes::Vector{StylingPass} # Styling passes to apply to input
6165
end
6266

6367
show(io::IO, x::Prompt) = show(io, string("Prompt(\"", prompt_string(x.prompt), "\",...)"))
@@ -565,6 +569,8 @@ function maybe_show_hint(s::PromptState)
565569
return nothing
566570
end
567571

572+
max_highlight_size::Int = 10000 # bytes
573+
568574
function refresh_multi_line(s::PromptState; kw...)
569575
if s.refresh_wait !== nothing
570576
close(s.refresh_wait)
@@ -611,6 +617,42 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf
611617
reader isa Base.TTY && !Base.ispty(reader)::Bool
612618
else false end
613619

620+
# Get the styling passes from the prompt
621+
prompt_obj = nothing
622+
if prompt isa PromptState
623+
prompt_obj = prompt.p
624+
elseif prompt isa PrefixSearchState || prompt isa SearchState
625+
if isdefined(prompt, :parent) && prompt.parent isa Prompt
626+
prompt_obj = prompt.parent
627+
end
628+
end
629+
630+
styled_buffer = AnnotatedString("")
631+
if buf.size > 0 && buf.size <= max_highlight_size
632+
full_input = String(buf.data[1:buf.size])
633+
if !isempty(full_input)
634+
passes = StylingPass[]
635+
context = StylingContext(buf_pos, regstart, regstop)
636+
637+
# Add prompt-specific styling passes if the prompt has them and styling is enabled
638+
enable_style_input = prompt_obj === nothing ? false :
639+
(isdefined(prompt_obj, :repl) && prompt_obj.repl !== nothing ?
640+
prompt_obj.repl.options.style_input : false)
641+
642+
if enable_style_input && prompt_obj !== nothing
643+
append!(passes, prompt_obj.styling_passes)
644+
end
645+
646+
if region_active
647+
push!(passes, RegionHighlightPass())
648+
end
649+
650+
if !isempty(passes)
651+
styled_buffer = apply_styling_passes(full_input, passes, context)
652+
end
653+
end
654+
end
655+
614656
# Now go through the buffer line by line
615657
seek(buf, 0)
616658
moreinput = true # add a blank line if there is a trailing newline on the last line
@@ -636,12 +678,26 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf
636678
llength = textwidth(line)
637679
slength = sizeof(line)
638680
cur_row += 1
639-
# lwrite: what will be written to termbuf
640-
lwrite = region_active ? highlight_region(line, regstart, regstop, written, slength) :
641-
line
681+
682+
# Extract the portion of styled_buffer corresponding to this line.
683+
if !isempty(styled_buffer)
684+
# Calculate byte positions for this line in the buffer
685+
line_start_byte = written + 1
686+
line_end_byte = written + slength
687+
688+
# Convert to valid character indices (handles UTF-8 boundaries)
689+
start_idx = thisind(styled_buffer, line_start_byte)
690+
end_idx = thisind(styled_buffer, line_end_byte)
691+
692+
lwrite = @view styled_buffer[start_idx:end_idx]
693+
else
694+
lwrite = line
695+
end
696+
642697
written += slength
643698
cmove_col(termbuf, lindent + 1)
644-
write(termbuf, lwrite)
699+
700+
write(IOContext(termbuf, :color => hascolor(terminal)), lwrite)
645701
# We expect to be line after the last valid output line (due to
646702
# the '\n' at the end of the previous line)
647703
if curs_row == -1
@@ -692,18 +748,6 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf
692748
return InputAreaState(cur_row, curs_row)
693749
end
694750

695-
function highlight_region(lwrite::Union{String,SubString{String}}, regstart::Int, regstop::Int, written::Int, slength::Int)
696-
if written <= regstop <= written+slength
697-
i = thisind(lwrite, regstop-written)
698-
lwrite = lwrite[1:i] * Base.disable_text_style[:reverse] * lwrite[nextind(lwrite, i):end]
699-
end
700-
if written <= regstart <= written+slength
701-
i = thisind(lwrite, regstart-written)
702-
lwrite = lwrite[1:i] * Base.text_colors[:reverse] * lwrite[nextind(lwrite, i):end]
703-
end
704-
return lwrite
705-
end
706-
707751
function refresh_multi_line(terminal::UnixTerminal, args...; kwargs...)
708752
outbuf = IOBuffer()
709753
termbuf = TerminalBuffer(outbuf)
@@ -999,7 +1043,9 @@ function edit_insert(s::PromptState, c::StringLike)
9991043
offset += position(buf) - beginofline(buf) # size of current line
10001044
spinner = '\0'
10011045
delayup = !eof(buf) || old_wait
1002-
if offset + textwidth(str) <= w && !(after == 0 && delayup)
1046+
# Disable fast path when syntax highlighting is enabled
1047+
use_fast_path = offset + textwidth(str) <= w && !(after == 0 && delayup) && !options(s).style_input
1048+
if use_fast_path
10031049
# Avoid full update when appending characters to the end
10041050
# and an update of curs_row isn't necessary (conservatively estimated)
10051051
write(termbuf, str)
@@ -2226,6 +2272,13 @@ function replace_line(s::PrefixSearchState, l::Union{String,SubString{String}})
22262272
nothing
22272273
end
22282274

2275+
function write_prompt(terminal, s::SearchState, color::Bool)
2276+
failed = s.failed ? "failed " : ""
2277+
promptstr = s.backward ? "($(failed)reverse-i-search)`" : "($(failed)forward-i-search)`"
2278+
write(terminal, promptstr)
2279+
return textwidth(promptstr)
2280+
end
2281+
22292282
function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState)
22302283
buf = IOBuffer()
22312284
unsafe_write(buf, pointer(s.query_buffer.data), s.query_buffer.ptr-1)
@@ -2236,9 +2289,7 @@ function refresh_multi_line(termbuf::TerminalBuffer, s::SearchState)
22362289
write(buf, read(s.response_buffer, String))
22372290
buf.ptr = offset + ptr - 1
22382291
s.response_buffer.ptr = ptr
2239-
failed = s.failed ? "failed " : ""
2240-
ias = refresh_multi_line(termbuf, s.terminal, buf, s.ias,
2241-
s.backward ? "($(failed)reverse-i-search)`" : "($(failed)forward-i-search)`")
2292+
ias = refresh_multi_line(termbuf, s.terminal, buf, s.ias, s)
22422293
s.ias = ias
22432294
return ias
22442295
end
@@ -2823,10 +2874,11 @@ function Prompt(prompt
28232874
on_enter = default_enter_cb,
28242875
on_done = ()->nothing,
28252876
hist = EmptyHistoryProvider(),
2826-
sticky = false)
2877+
sticky = false,
2878+
styling_passes = StylingPass[])
28272879

28282880
return Prompt(prompt, prompt_prefix, prompt_suffix, output_prefix, output_prefix_prefix, output_prefix_suffix,
2829-
keymap_dict, repl, complete, on_enter, on_done, hist, sticky)
2881+
keymap_dict, repl, complete, on_enter, on_done, hist, sticky, styling_passes)
28302882
end
28312883

28322884
run_interface(::Prompt) = nothing

stdlib/REPL/src/REPL.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ using Base.Terminals
6666
abstract type AbstractREPL end
6767

6868
include("options.jl")
69+
include("StylingPasses.jl")
70+
using .StylingPasses
6971

7072
include("LineEdit.jl")
7173
using .LineEdit
@@ -1329,7 +1331,11 @@ function setup_interface(
13291331
(repl.envcolors ? Base.input_color : repl.input_color) : "",
13301332
repl = repl,
13311333
complete = replc,
1332-
on_enter = return_callback)
1334+
on_enter = return_callback,
1335+
styling_passes = StylingPasses.StylingPass[
1336+
StylingPasses.SyntaxHighlightPass(),
1337+
StylingPasses.EnclosingParenHighlightPass()
1338+
])
13331339

13341340
# Setup help mode
13351341
help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),

0 commit comments

Comments
 (0)