Repository: domluna/JuliaFormatter.jl
Pull request #146 (2019-12-03 06:13:53)

More options related to whitespace
1. Add whitespace in type definitions

```julia
Foo{A, B} instead of Foo{A,B}

or 

Foo{A <: B, C} instead of Foo{A<:B,C}
```

2. Add whitespace for binops in indices and ranges

```julia
arr[a + b] instead of arr[a+b]

arr[(a + b):(c + d)] instead of arr[a+b:c+d]
```

Notice the added parens :), otherwise this wouldn't look as good.

- [x] update docs
- [x] update https://github.com/julia-actions/julia-format


Patch:
diff --git a/Project.toml b/Project.toml
index 99a38310..9c1bfe25 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "JuliaFormatter"
 uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
 authors = ["Dominique Luna <dluna132@gmail.com>"]
-version = "0.1.37"
+version = "0.1.38"
 
 [deps]
 CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
diff --git a/bin/format.jl b/bin/format.jl
index efef17cd..d6dcff47 100755
--- a/bin/format.jl
+++ b/bin/format.jl
@@ -1,5 +1,4 @@
 #!/usr/bin/env julia
-
 using JuliaFormatter
 
 help = """
@@ -7,8 +6,8 @@ JuliaFormatter formats Julia (.jl) programs. The formatter is width-sensitive.
 
 Without an explicit file or path, this help message is written to stdout.
 Given a file, it operates on that file; given a directory, it operates on
-all .jl files in that directory, recursively (Files starting with a nofmt comment are ignored).
-By default, JuliaFormatter overwrites files with the reformatted source.
+all .jl files in that directory, recursively.  By default, JuliaFormatter overwrites
+files with the reformatted source.
 
 Usage:
 
@@ -18,20 +17,35 @@ Flags:
 
     -i, --indent
         The number of spaces used for an indentation.
+
     -m, --margin
         The maximum number of characters of code on a single line.  Lines over the
         limit will be wrapped if possible. There are cases where lines cannot be wrapped
         and they will still end up wider than the requested margin.
+
     -v, --verbose
         Print the name of the files being formatted with relevant details.
+
     -h, --help
         Print this message.
+
     -o, --overwrite
         Writes the formatted source to a new file where the original
         filename is suffixed with _fmt, i.e. `filename_fmt.jl`.
+
     --always_for_in
         Always replaces `=` with `in` for `for` loops.
-        For example, `for i = 1:10` will be transformed to `for i in 1:10`.
+        Example: `for i = 1:10` will be transformed to `for i in 1:10`.
+
+    --whitespace_typedefs
+        Add whitespace in type definitions.
+        Example: `Union{A <: B, C}` to `Union{A<:B,C}`.
+
+    --whitespace_ops_in_indices`
+        Add whitespace to binary ops in indices.
+        Example: `arr[a + b]` to `arr[a+b]`.
+        Additionally, if there's a colon `:` involved, parenthesis will be added to the LHS and RHS.
+        Example: `arr[(i1 + i2):(i3 + i4)]` instead of `arr[i1+i2:i3+i4]`.
 """
 
 function parse_opts!(args::Vector{String})
@@ -55,10 +69,20 @@ function parse_opts!(args::Vector{String})
             opt = :overwrite
         elseif arg == "--always_for_in"
             opt = :always_for_in
+        elseif arg == "--whitespace_typedefs"
+            opt = :whitespace_typedefs
+        elseif arg == "--whitespace_ops_in_indices"
+            opt = :whitespace_ops_in_indices
         else
             error("invalid option $arg")
         end
-        if opt in (:verbose, :help, :always_for_in)
+        if opt in (
+            :verbose,
+            :help,
+            :always_for_in,
+            :whitespace_typedefs,
+            :whitespace_ops_in_indices,
+        )
             opts[opt] = true
             deleteat!(args, i)
         elseif opt == :overwrite
diff --git a/docs/src/index.md b/docs/src/index.md
index 67f385d1..f7c4cd51 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -2,7 +2,7 @@
 
 [![Build Status](https://travis-ci.org/domluna/JuliaFormatter.jl.svg?branch=master)](https://travis-ci.org/domluna/JuliaFormatter.jl)
 
-Width-sensitive formatter for Julia code. Inspired by gofmt, refmt, and black built with [`CSTParser`](https://github.com/ZacLN/CSTParser.jl).
+Width-sensitive formatter for Julia code. Inspired by gofmt, refmt, and black. Built with [`CSTParser`](https://github.com/ZacLN/CSTParser.jl).
 
 ## Installation
 
@@ -10,6 +10,15 @@ Width-sensitive formatter for Julia code. Inspired by gofmt, refmt, and black bu
 ]add JuliaFormatter
 ```
 
+## Quick Start
+
+```julia
+julia> using JuliaFormatter
+
+# Recursively formats all Julia files in the current directory
+julia> format(".")
+```
+
 ## Usage
 
 `JuliaFormatter` exports `format_text`, `format_file` and `format`:
@@ -20,38 +29,59 @@ format_text(
     indent = 4,
     margin = 92,
     always_for_in = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
 )
 
 format_file(
     file::AbstractString;
-    indent = 4,
-    margin = 92,
     overwrite = true,
     verbose = false,
+    indent = 4,
+    margin = 92,
     always_for_in = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
 )
 
 format(
     paths...;
-    indent = 4,
-    margin = 92,
     overwrite = true,
     verbose = false,
+    indent = 4,
+    margin = 92,
     always_for_in = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
 )
 ```
 
 The `text` argument to `format_text` is a string containing the code to be formatted; the formatted code is retuned as a new string. The `file` argument to `format_file` is the path of a file to be formatted. The `format` function is either called with a singe string to format if it is a `.jl` file or to recuse into looking for `.jl` files if it is a directory. It can also be called with a collection of such paths to iterate over.
 
-*Options:*
+### File Options
+
+If `overwrite` is `true` the file will be reformatted in place, overwriting
+the existing file; if it is `false`, the formatted version of `foo.jl` will
+be written to `foo_fmt.jl` instead.
+
+If `verbose` is `true` details related to formatting the file will be printed
+to `stdout`.
+
+### Formatting Options
+
+`indent` - the number of spaces used for an indentation.
+
+`margin` - the maximum length of a line. Code exceeding this margin will be formatted
+across multiple lines.
 
-* `indent` - The number of spaces used for an indentation.
-* `margin` - The maximum number of characters of code on a single line. Lines over
-the limit will be wrapped if possible. There are cases where lines cannot be wrapped
-and they will still end up wider than the requested margin.
-* `overwrite` - If the file should be overwritten by the formatted output. If set to false, the formatted version of file named `foo.jl` will be written to `foo_fmt.jl`.
-* `verbose` - Whether to print the name of the file being formatted along with relevant details to `stdout`.
-* `always_for_in` - Always use `in` keyword for `for` loops. This defaults to `false`.
+If `always_for_in` is true `=` is always replaced with `in` if part of a
+`for` loop condition.  For example, `for i = 1:10` will be transformed
+to `for i in 1:10`.
 
-There is also a command-line tool `bin/format.jl` which can be invoked with `-i`/`--indent` and `-m`/`--margin` and with paths which will be passed to `format`.
+If `whitespace_typedefs` is true, whitespace is added for type definitions.
+Make this `true` if you prefer `Union{A <: B, C}` to `Union{A<:B,C}`.
 
+If `whitespace_ops_in_indices` is true, whitespace is added for binary operations
+in indices. Make this `true` if you prefer `arr[a + b]` to `arr[a+b]`. Additionally,
+if there's a colon `:` involved, parenthesis will be added to the LHS and RHS.
+Example: `arr[(i1 + i2):(i3 + i4)]` instead of `arr[i1+i2:i3+i4]`.
diff --git a/src/JuliaFormatter.jl b/src/JuliaFormatter.jl
index 9715480a..7a0dab83 100644
--- a/src/JuliaFormatter.jl
+++ b/src/JuliaFormatter.jl
@@ -154,6 +154,12 @@ function Document(text::AbstractString)
     Document(text, ranges, line_to_range, lit_strings, comments, semicolons, format_skips)
 end
 
+Base.@kwdef struct Options
+    always_for_in::Bool = false
+    whitespace_typedefs::Bool = false
+    whitespace_ops_in_indices::Bool = false
+end
+
 mutable struct State
     doc::Document
     indent_size::Int
@@ -161,13 +167,12 @@ mutable struct State
     offset::Int
     line_offset::Int
     margin::Int
-    always_for_in::Bool
-    # if true will output the formatted text
+    # If true will output the formatted text
     # otherwise will output the current text
     on::Bool
+    opts::Options
 end
-State(doc, indent_size, margin; always_for_in = false) =
-    State(doc, indent_size, 0, 1, 0, margin, always_for_in, true)
+State(doc, indent_size, margin, opts) = State(doc, indent_size, 0, 1, 0, margin, true, opts)
 
 @inline nspaces(s::State) = s.indent
 @inline hascomment(d::Document, line::Integer) = haskey(d.comments, line)
@@ -193,23 +198,39 @@ include("print.jl")
         indent::Int = 4,
         margin::Int = 92,
         always_for_in::Bool = false,
-    ) :: String
+        whitespace_typedefs::Bool = false,
+        whitespace_ops_in_indices::Bool = false,
+    )::String
 
 Formats a Julia source passed in as a string, returning the formatted
-code as another string. The formatting options are:
+code as another string.
+
+### Formatting Options
 
-- `indent` which defaults to 4 spaces
-- `margin` which defaults to 92 columns
+`indent` - the number of spaces used for an indentation.
+
+`margin` - the maximum length of a line. Code exceeding this margin will be formatted
+across multiple lines.
 
 If `always_for_in` is true `=` is always replaced with `in` if part of a
 `for` loop condition.  For example, `for i = 1:10` will be transformed
 to `for i in 1:10`.
+
+If `whitespace_typedefs` is true, whitespace is added for type definitions.
+Make this `true` if you prefer `Union{A <: B, C}` to `Union{A<:B,C}`.
+
+If `whitespace_ops_in_indices` is true, whitespace is added for binary operations
+in indices. Make this `true` if you prefer `arr[a + b]` to `arr[a+b]`. Additionally,
+if there's a colon `:` involved, parenthesis will be added to the LHS and RHS.
+Example: `arr[(i1 + i2):(i3 + i4)]` instead of `arr[i1+i2:i3+i4]`.
 """
 function format_text(
     text::AbstractString;
     indent::Int = 4,
     margin::Int = 92,
     always_for_in::Bool = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
 )
     isempty(text) && return text
 
@@ -219,8 +240,12 @@ function format_text(
     # no actual code
     x.args[1].kind === Tokens.NOTHING && length(x) == 1 && return text
 
-    d = Document(text)
-    s = State(d, indent, margin, always_for_in = always_for_in)
+    opts = Options(
+        always_for_in = always_for_in,
+        whitespace_typedefs = whitespace_typedefs,
+        whitespace_ops_in_indices = whitespace_ops_in_indices,
+    )
+    s = State(Document(text), indent, margin, opts)
     t = pretty(x, s)
     hascomment(s.doc, t.endline) && (add_node!(t, InlineComment(t.endline), s))
     nest!(t, s)
@@ -252,19 +277,18 @@ end
 """
     format_file(
         filename::AbstractString;
-        indent::Integer = 4,
-        margin::Integer = 92,
         overwrite::Bool = true,
         verbose::Bool = false,
+        indent::Integer = 4,
+        margin::Integer = 92,
         always_for_in::Bool = false,
+        whitespace_typedefs::Bool = false,
+        whitespace_ops_in_indices::Bool = false,
     )
 
 Formats the contents of `filename` assuming it's a Julia source file.
 
-The formatting options are:
-
-- `indent` which defaults to 4 spaces
-- `margin` which defaults to 92 columns
+### File Options
 
 If `overwrite` is `true` the file will be reformatted in place, overwriting
 the existing file; if it is `false`, the formatted version of `foo.jl` will
@@ -273,25 +297,49 @@ be written to `foo_fmt.jl` instead.
 If `verbose` is `true` details related to formatting the file will be printed
 to `stdout`.
 
+### Formatting Options
+
+`indent` - the number of spaces used for an indentation.
+
+`margin` - the maximum length of a line. Code exceeding this margin will be formatted
+across multiple lines.
+
 If `always_for_in` is true `=` is always replaced with `in` if part of a
 `for` loop condition.  For example, `for i = 1:10` will be transformed
 to `for i in 1:10`.
+
+If `whitespace_typedefs` is true, whitespace is added for type definitions.
+Make this `true` if you prefer `Union{A <: B, C}` to `Union{A<:B,C}`.
+
+If `whitespace_ops_in_indices` is true, whitespace is added for binary operations
+in indices. Make this `true` if you prefer `arr[a + b]` to `arr[a+b]`. Additionally,
+if there's a colon `:` involved, parenthesis will be added to the LHS and RHS.
+Example: `arr[(i1 + i2):(i3 + i4)]` instead of `arr[i1+i2:i3+i4]`.
 """
 function format_file(
     filename::AbstractString;
-    indent::Integer = 4,
-    margin::Integer = 92,
     overwrite::Bool = true,
     verbose::Bool = false,
+    indent::Integer = 4,
+    margin::Integer = 92,
     always_for_in::Bool = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
 )
     path, ext = splitext(filename)
     if ext != ".jl"
         error("$filename must be a Julia (.jl) source file")
     end
-    verbose && println("Formatting $filename with indent = $indent, margin = $margin")
+    verbose && println("Formatting $filename")
     str = String(read(filename))
-    str = format_text(str, indent = indent, margin = margin, always_for_in = always_for_in)
+    str = format_text(
+        str,
+        indent = indent,
+        margin = margin,
+        always_for_in = always_for_in,
+        whitespace_typedefs = whitespace_typedefs,
+        whitespace_ops_in_indices = whitespace_ops_in_indices,
+    )
     str = replace(str, r"\n*$" => "\n")
     overwrite ? write(filename, str) : write(path * "_fmt" * ext, str)
     nothing
diff --git a/src/nest.jl b/src/nest.jl
index 2a114dce..ef5f77e5 100644
--- a/src/nest.jl
+++ b/src/nest.jl
@@ -494,6 +494,9 @@ function n_binarycall!(x, s)
             ) || arg2.typ === CSTParser.UnaryOpCall ||
                arg2.typ === CSTParser.ChainOpCall || arg2.typ === CSTParser.Comparison
                 line_margin += length(x[end])
+            elseif arg2.typ === CSTParser.Do && is_iterable(arg2[1])
+                rw, _ = length_to(x, [NEWLINE], start = i2 + 1)
+                line_margin += rw
             elseif is_block(cst)
                 idx = findfirst(n -> n.typ === NEWLINE, arg2.nodes)
                 if idx === nothing
diff --git a/src/pretty.jl b/src/pretty.jl
index 4f204a5c..fb628b05 100644
--- a/src/pretty.jl
+++ b/src/pretty.jl
@@ -58,6 +58,7 @@ InlineComment(line) = PTree(INLINECOMMENT, line, line, 0, 0, "", nothing, nothin
 
 Base.length(pt::PTree) = pt.len
 
+is_leaf(cst::CSTParser.EXPR) = cst.args === nothing
 is_leaf(pt::PTree) = pt.nodes === nothing
 empty_start(pt::PTree) = pt.startline == 1 && pt.endline == 1 && pt.val == ""
 
@@ -317,7 +318,6 @@ is_opener(x::CSTParser.EXPR) =
     x.kind === Tokens.LBRACE || x.kind === Tokens.LPAREN || x.kind === Tokens.LSQUARE
 
 function pretty(x::CSTParser.EXPR, s::State)
-    # @debug "" x.typ x.args
     if x.typ === CSTParser.IDENTIFIER
         return p_identifier(x, s)
     elseif x.typ === CSTParser.OPERATOR
@@ -1039,7 +1039,7 @@ function p_loop(x, s)
     add_node!(t, pretty(x.args[1], s), s)
     add_node!(t, Whitespace(1), s)
     if x.args[1].kind === Tokens.FOR
-        eq_to_in_normalization!(x.args[2], s.always_for_in)
+        eq_to_in_normalization!(x.args[2], s.opts.always_for_in)
     end
     if x.args[2].typ === CSTParser.Block
         add_node!(t, p_block(x.args[2], s, join_body = true), s, join_lines = true)
@@ -1199,13 +1199,16 @@ end
 # ChainOpCall/Comparison
 function p_chaincall(x, s; nonest = false, nospace = false)
     t = PTree(x, nspaces(s))
+    nws = nospace ? 0 : 1
     for (i, a) in enumerate(x)
         n = pretty(a, s)
         if a.typ === CSTParser.OPERATOR
             !nospace && add_node!(t, Whitespace(1), s)
             add_node!(t, n, s, join_lines = true)
-            if !(nospace && nonest)
-                add_node!(t, Placeholder(1), s)
+            if nonest
+                add_node!(t, Whitespace(nws), s)
+            else
+                add_node!(t, Placeholder(nws), s)
             end
         elseif i == length(x) - 1 && is_punc(a) && is_punc(x.args[i+1])
             add_node!(t, n, s, join_lines = true)
@@ -1219,13 +1222,27 @@ end
 # ColonOpCall
 function p_coloncall(x, s)
     t = PTree(x, nspaces(s))
+    nospace = !s.opts.whitespace_ops_in_indices
     for a in x
         if a.typ === CSTParser.BinaryOpCall
-            n = p_binarycall(a, s, nonest = true, nospace = true)
+            n = p_binarycall(a, s, nonest = true, nospace = nospace)
+        elseif a.typ === CSTParser.InvisBrackets
+            n = p_invisbrackets(a, s, nonest = true, nospace = nospace)
+        elseif a.typ === CSTParser.ChainOpCall || a.typ === CSTParser.Comparison
+            n = p_chaincall(a, s, nonest = true, nospace = nospace)
         else
             n = pretty(a, s)
         end
-        add_node!(t, n, s, join_lines = true)
+
+        if s.opts.whitespace_ops_in_indices && !is_leaf(n) && !is_iterable(n)
+            paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, "(")
+            add_node!(t, paren, s, join_lines = true)
+            add_node!(t, n, s, join_lines = true)
+            paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, ")")
+            add_node!(t, paren, s, join_lines = true)
+        else
+            add_node!(t, n, s, join_lines = true)
+        end
     end
     t
 end
@@ -1283,22 +1300,35 @@ end
 # BinaryOpCall
 function p_binarycall(x, s; nonest = false, nospace = false)
     t = PTree(x, nspaces(s))
-    op = x.args[2]
+    op = x[2]
     nonest = nonest || op.kind === Tokens.COLON
-    if x.parent.typ === CSTParser.Curly && op.kind in (Tokens.ISSUBTYPE, Tokens.ISSUPERTYPE)
+    if x.parent.typ === CSTParser.Curly &&
+       op.kind in (Tokens.ISSUBTYPE, Tokens.ISSUPERTYPE) && !s.opts.whitespace_typedefs
         nospace = true
     elseif op.kind === Tokens.COLON
         nospace = true
     end
+    nospace_args = s.opts.whitespace_ops_in_indices ? false : nospace
 
-    if x.args[1].typ === CSTParser.BinaryOpCall
-        add_node!(t, p_binarycall(x.args[1], s, nonest = nonest, nospace = nospace), s)
-    elseif x.args[1].typ === CSTParser.InvisBrackets
-        add_node!(t, p_invisbrackets(x.args[1], s, nonest = nonest, nospace = nospace), s)
-    elseif x.args[1].typ === CSTParser.ChainOpCall
-        add_node!(t, p_chaincall(x.args[1], s, nonest = nonest, nospace = nospace), s)
+    if x[1].typ === CSTParser.BinaryOpCall
+        n = p_binarycall(x[1], s, nonest = nonest, nospace = nospace_args)
+    elseif x[1].typ === CSTParser.InvisBrackets
+        n = p_invisbrackets(x[1], s, nonest = nonest, nospace = nospace_args)
+    elseif x[1].typ === CSTParser.ChainOpCall || x[1].typ === CSTParser.Comparison
+        n = p_chaincall(x[1], s, nonest = nonest, nospace = nospace_args)
     else
-        add_node!(t, pretty(x.args[1], s), s)
+        n = pretty(x[1], s)
+    end
+
+    if op.kind === Tokens.COLON &&
+       s.opts.whitespace_ops_in_indices && !is_leaf(x[1]) && !is_iterable(x[1])
+        paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, "(")
+        add_node!(t, paren, s)
+        add_node!(t, n, s, join_lines = true)
+        paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, ")")
+        add_node!(t, paren, s, join_lines = true)
+    else
+        add_node!(t, n, s)
     end
 
     narg2 = nest_arg2(x)
@@ -1306,7 +1336,7 @@ function p_binarycall(x, s; nonest = false, nospace = false)
     nest = (nestable(x) && !nonest) || narg2
     # @info "" nestable(x) !nonest narg2 nest x[2]
 
-    if op.fullspan == 0 && x.args[3].typ === CSTParser.IDENTIFIER
+    if op.fullspan == 0 && x[3].typ === CSTParser.IDENTIFIER
         # do nothing
     elseif op.kind === Tokens.EX_OR
         add_node!(t, Whitespace(1), s)
@@ -1326,22 +1356,32 @@ function p_binarycall(x, s; nonest = false, nospace = false)
         nest ? add_node!(t, Placeholder(1), s) : add_node!(t, Whitespace(1), s)
     end
 
+    if x[3].typ === CSTParser.BinaryOpCall
+        n = p_binarycall(x[3], s, nonest = nonest, nospace = nospace_args)
+    elseif x[3].typ === CSTParser.InvisBrackets
+        n = p_invisbrackets(x[3], s, nonest = nonest, nospace = nospace_args)
+    elseif x[3].typ === CSTParser.ChainOpCall || x[3].typ === CSTParser.Comparison
+        n = p_chaincall(x[3], s, nonest = nonest, nospace = nospace_args)
+    else
+        n = pretty(x[3], s)
+    end
 
-    if x.args[3].typ === CSTParser.BinaryOpCall
-        n = p_binarycall(x.args[3], s, nonest = nonest, nospace = nospace)
-    elseif x.args[3].typ === CSTParser.InvisBrackets
-        n = p_invisbrackets(x.args[3], s, nonest = nonest, nospace = nospace)
-    elseif x.args[3].typ === CSTParser.ChainOpCall
-        n = p_chaincall(x.args[3], s, nonest = nonest, nospace = nospace)
+    if op.kind === Tokens.COLON &&
+       s.opts.whitespace_ops_in_indices && !is_leaf(x[3]) && !is_iterable(x[3])
+        paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, "(")
+        add_node!(t, paren, s, join_lines = true)
+        add_node!(t, n, s, join_lines = true)
+        paren = PTree(CSTParser.PUNCTUATION, n.startline, n.startline, ")")
+        add_node!(t, paren, s, join_lines = true)
     else
-        n = pretty(x.args[3], s)
+        add_node!(t, n, s, join_lines = true)
     end
-    add_node!(t, n, s, join_lines = true)
 
     if nest
         # for indent, will be converted to `indent_size` if needed
         insert!(t.nodes, length(t.nodes), Placeholder(0))
     end
+
     t
 end
 
@@ -1373,6 +1413,7 @@ function p_wherecall(x, s)
         join_lines = true,
     )
 
+    nws = s.opts.whitespace_typedefs ? 1 : 0
     # @debug "" nest in_braces x.args[3].val == "{" x.args[end].val
     for (i, a) in enumerate(x.args[3:end])
         if is_opener(a) && nest
@@ -1386,9 +1427,14 @@ function p_wherecall(x, s)
             s.indent -= s.indent_size
         elseif CSTParser.is_comma(a) && !is_punc(x.args[i+3])
             add_node!(t, pretty(a, s), s, join_lines = true)
-            add_node!(t, Placeholder(0), s)
+            add_node!(t, Placeholder(nws), s)
         elseif a.typ === CSTParser.BinaryOpCall
-            add_node!(t, p_binarycall(a, s, nospace = true), s, join_lines = true)
+            add_node!(
+                t,
+                p_binarycall(a, s, nospace = !s.opts.whitespace_typedefs),
+                s,
+                join_lines = true,
+            )
         else
             add_node!(t, pretty(a, s), s, join_lines = true)
         end
@@ -1435,6 +1481,7 @@ function p_curly(x, s)
     args = get_args(x)
     nest = length(args) > 0 && !(length(args) == 1 && unnestable_arg(x[1]))
 
+    nws = s.opts.whitespace_typedefs ? 1 : 0
     if nest
         add_node!(t, Placeholder(0), s)
     end
@@ -1446,7 +1493,7 @@ function p_curly(x, s)
             add_node!(t, pretty(a, s), s, join_lines = true)
         elseif CSTParser.is_comma(a) && i < length(x) - 3 && !is_punc(x.args[i+3])
             add_node!(t, pretty(a, s), s, join_lines = true)
-            add_node!(t, Placeholder(0), s)
+            add_node!(t, Placeholder(nws), s)
         else
             add_node!(t, pretty(a, s), s, join_lines = true)
         end
@@ -1632,6 +1679,7 @@ end
 function p_ref(x, s)
     t = PTree(x, nspaces(s))
     nest = length(x) > 5 && !(length(x) == 5 && unnestable_arg(x[3]))
+    nospace = !s.opts.whitespace_ops_in_indices
     for (i, a) in enumerate(x)
         if is_closer(a) && nest
             add_node!(t, TrailingComma(), s)
@@ -1646,14 +1694,14 @@ function p_ref(x, s)
         elseif a.typ === CSTParser.BinaryOpCall
             add_node!(
                 t,
-                p_binarycall(a, s, nonest = true, nospace = true),
+                p_binarycall(a, s, nonest = true, nospace = nospace),
                 s,
                 join_lines = true,
             )
         elseif a.typ === CSTParser.InvisBrackets
             add_node!(
                 t,
-                p_invisbrackets(a, s, nonest = true, nospace = true),
+                p_invisbrackets(a, s, nonest = true, nospace = nospace),
                 s,
                 join_lines = true,
             )
@@ -1758,7 +1806,7 @@ function p_gen(x, s)
             add_node!(t, Whitespace(1), s)
             if a.kind === Tokens.FOR
                 for j = i+1:length(x)
-                    eq_to_in_normalization!(x.args[j], s.always_for_in)
+                    eq_to_in_normalization!(x.args[j], s.opts.always_for_in)
                 end
             end
         elseif a.typ === CSTParser.BinaryOpCall


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 6922a195..e15c9d6f 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -2,21 +2,41 @@ using JuliaFormatter
 using CSTParser
 using Test
 
-fmt1(s, i = 4, m = 80, always_for_in = false) =
-    JuliaFormatter.format_text(s, indent = i, margin = m, always_for_in = always_for_in)
+fmt1(
+    s,
+    i = 4,
+    m = 80,
+    always_for_in = false,
+    whitespace_typedefs = false,
+    whitespace_ops_in_indices = false,
+) = JuliaFormatter.format_text(
+    s,
+    indent = i,
+    margin = m,
+    always_for_in = always_for_in,
+    whitespace_typedefs = whitespace_typedefs,
+    whitespace_ops_in_indices = whitespace_ops_in_indices,
+)
 
 # Verifies formatting the formatted text
 # results in the same output
-function fmt(s; i = 4, m = 80, always_for_in = false)
-    s1 = fmt1(s, i, m, always_for_in)
-    fmt1(s1, i, m, always_for_in)
+function fmt(
+    s;
+    i = 4,
+    m = 80,
+    always_for_in = false,
+    whitespace_typedefs = false,
+    whitespace_ops_in_indices = false,
+)
+    s1 = fmt1(s, i, m, always_for_in, whitespace_typedefs, whitespace_ops_in_indices)
+    fmt1(s1, i, m, always_for_in, whitespace_typedefs, whitespace_ops_in_indices)
 end
 fmt(s, i, m) = fmt(s; i = i, m = m)
-fmt1(s, i, m) = fmt1(s, i, m, false)
+fmt1(s, i, m) = fmt1(s, i, m, false, false, false)
 
 function run_pretty(text::String, print_width::Int)
     d = JuliaFormatter.Document(text)
-    s = JuliaFormatter.State(d, 4, print_width)
+    s = JuliaFormatter.State(d, 4, print_width, JuliaFormatter.Options())
     x = CSTParser.parse(text, true)
     t = JuliaFormatter.pretty(x, s)
     t
@@ -24,7 +44,7 @@ end
 
 function run_nest(text::String, print_width::Int)
     d = JuliaFormatter.Document(text)
-    s = JuliaFormatter.State(d, 4, print_width)
+    s = JuliaFormatter.State(d, 4, print_width, JuliaFormatter.Options())
     x = CSTParser.parse(text, true)
     t = JuliaFormatter.pretty(x, s)
     JuliaFormatter.nest!(t, s)
@@ -1066,23 +1086,22 @@ end
             body
         end"""
         str = """
-        model =
-            SDDP.LinearPolicyGraph(
-                stages = 2,
-                lower_bound = 1,
-                direct_mode = false,
-            ) do (
-                subproblem1,
-                subproblem2,
-                subproblem3,
-                subproblem4,
-                subproblem5,
-                subproblem6,
-                subproblem7,
-                subproblem8,
-            )
-                body
-            end"""
+        model = SDDP.LinearPolicyGraph(
+            stages = 2,
+            lower_bound = 1,
+            direct_mode = false,
+        ) do (
+            subproblem1,
+            subproblem2,
+            subproblem3,
+            subproblem4,
+            subproblem5,
+            subproblem6,
+            subproblem7,
+            subproblem8,
+        )
+            body
+        end"""
         @test fmt(str_) == str
 
         str_ = """
@@ -1090,14 +1109,13 @@ end
             body
         end"""
         str = """
-        model =
-            SDDP.LinearPolicyGraph(
-                stages = 2,
-                lower_bound = 1,
-                direct_mode = false,
-            ) do subproblem1, subproblem2
-                body
-            end"""
+        model = SDDP.LinearPolicyGraph(
+            stages = 2,
+            lower_bound = 1,
+            direct_mode = false,
+        ) do subproblem1, subproblem2
+            body
+        end"""
         @test fmt(str_) == str
 
     end
@@ -3868,4 +3886,62 @@ some_function(
 
     end
 
+    @testset "whitespace typedefs option" begin
+        str_ = "Foo{A,B,C}"
+        str = "Foo{A, B, C}"
+        @test fmt(str_, whitespace_typedefs = true) == str
+
+        str_ = """
+        struct Foo{A<:Bar,Union{B<:Fizz,C<:Buzz},<:Any}
+            a::A
+        end"""
+        str = """
+        struct Foo{A <: Bar, Union{B <: Fizz, C <: Buzz}, <:Any}
+            a::A
+        end"""
+        @test fmt(str_, whitespace_typedefs = true) == str
+
+        str_ = """
+        function foo() where {A,B,C{D,E,F{G,H,I},J,K},L,M<:N,Y>:Z}
+            body
+        end
+        """
+        str = """
+        function foo() where {A, B, C{D, E, F{G, H, I}, J, K}, L, M <: N, Y >: Z}
+            body
+        end
+        """
+        @test fmt(str_, whitespace_typedefs = true) == str
+
+        str_ = "foo() where {A,B,C{D,E,F{G,H,I},J,K},L,M<:N,Y>:Z} = body"
+        str = "foo() where {A, B, C{D, E, F{G, H, I}, J, K}, L, M <: N, Y >: Z} = body"
+        @test fmt(str_, whitespace_typedefs = true) == str
+    end
+
+    @testset "whitespace ops in indices option" begin
+        str = "arr[1 + 2]"
+        @test fmt("arr[1+2]", m = 1, whitespace_ops_in_indices = true) == str
+
+        str = "arr[(1 + 2)]"
+        @test fmt("arr[(1+2)]", m = 1, whitespace_ops_in_indices = true) == str
+
+        str_ = "arr[1:2*num_source*num_dump-1]"
+        str = "arr[1:(2 * num_source * num_dump - 1)]"
+        @test fmt(str_, m = 1, whitespace_ops_in_indices = true) == str
+
+        str_ = "arr[2*num_source*num_dump-1:1]"
+        str = "arr[(2 * num_source * num_dump - 1):1]"
+        @test fmt(str_, m = 1, whitespace_ops_in_indices = true) == str
+
+        str = "arr[(a + b):c]"
+        @test fmt("arr[(a+b):c]", m = 1, whitespace_ops_in_indices = true) == str
+
+        str = "arr[a in b]"
+        @test fmt(str, m = 1, whitespace_ops_in_indices = true) == str
+
+        str_ = "a:b+c:d-e"
+        str = "a:(b + c):(d - e)"
+        @test fmt(str_, m = 1, whitespace_ops_in_indices = true) == str
+    end
+
 end