diff --git a/docs/Documentation.md b/docs/Documentation.md
index 4102ad1dd1..0b5c97ab9c 100644
--- a/docs/Documentation.md
+++ b/docs/Documentation.md
@@ -472,8 +472,9 @@ let WebApp =
### fsharp_max_record_width
-Control the maximum width for which records should be in one line.
-Default = 40.
+Control the maximum width for which records should be in one line. Default = 40.
+Requires `fsharp_record_multiline_formatter` to be `character_width` to take
+effect.
`defaultConfig`
@@ -496,6 +497,99 @@ let myInstance =
Length = 90 }
```
+### fsharp_max_record_number_of_items
+
+Control the maximum number of fields for which records should be in one line.
+Default = 1. Requires `fsharp_array_or_list_multiline_formatter` to be
+`number_of_items` to take effect.
+
+`defaultConfig`
+
+```fsharp
+type R = { x: int }
+
+type S = { x: int; y: string }
+
+type T = { x: int; y: string; z: float }
+
+let myRecord = { r = 3 }
+
+let myRecord' = { r with x = 3 }
+
+let myRecord'' = { r with x = 3; y = "hello" }
+
+let myRecord''' = { r with x = 3; y = "hello"; z = 0.0 }
+```
+
+`{ defaultConfig with MaxRecordSize = 2; RecordMultilineFormatter =
+MultilineFormatterType.NumberOfItems }`
+
+```fsharp
+type R = { x: int }
+
+type S = { x: int; y: string }
+
+type T =
+ { x: int
+ y: string
+ z: float }
+
+let myRecord = { r = 3 }
+
+let myRecord' = { r with x = 3 }
+
+let myRecord'' = { r with x = 3; y = "hello" }
+
+let myRecord''' =
+ { r with
+ x = 3
+ y = "hello"
+ z = 0.0 }
+```
+
+### fsharp_record_multiline_formatter
+
+Split records expressions/statements into multiple lines based on the given
+condition. `character_width` uses character count of the expression, controlled
+by `fsharp_max_record_width`. `number_of_items` uses the number of fields in the
+record, controlled by `fsharp_max_record_number_of_items`. Default =
+`character_width`. Note that in either case, record expressions/statements are
+still governed by `max_line_length`.
+
+`defaultConfig`
+
+```fsharp
+type R = { x: int }
+
+type S = { x: int; y: string }
+
+let myRecord = { r = 3 }
+
+let myRecord' = { r with x = 3 }
+
+let myRecord'' = { r with x = 3; y = "hello" }
+```
+
+`{ defaultConfig with RecordMultilineFormatter =
+MultilineFormatterType.NumberOfItems }`
+
+```fsharp
+type R = { x: int }
+
+type S =
+ { x: int
+ y: string }
+
+let myRecord = { x = 3 }
+
+let myRecord' = { r with x = 3 }
+
+let myRecord'' =
+ { r with
+ x = 3
+ y = "hello" }
+```
+
### fsharp_max_array_or_list_width
Control the maximum width for which lists and arrays can be in one line. Default
diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj
index a88e43dfd1..8946dce738 100644
--- a/src/Fantomas.Tests/Fantomas.Tests.fsproj
+++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj
@@ -74,7 +74,8 @@
-
+
+
diff --git a/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs b/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs
index 77e11bd4ca..5fa2626c12 100644
--- a/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs
+++ b/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs
@@ -204,7 +204,7 @@ let ``print default editorconfig settings`` () =
|> printfn "%s"
[]
-let ``number_of_items parsing tests`` () =
+let ``list and array number_of_items parsing tests`` () =
let editorConfig = """
[*.fs]
fsharp_array_or_list_multiline_formatter = number_of_items
@@ -225,7 +225,7 @@ fsharp_max_array_or_list_number_of_items = 4
== NumberOfItems
[]
-let ``character_width parsing test with single option`` () =
+let ``list and array character_width parsing test with single option`` () =
let editorConfig = """
[*.fs]
fsharp_max_array_or_list_width = 123
@@ -240,3 +240,40 @@ fsharp_max_array_or_list_width = 123
EditorConfig.readConfiguration fsharpFile.FSharpFile
config.MaxArrayOrListWidth == 123
+
+[]
+let ``record number_of_items parsing tests`` () =
+ let editorConfig = """
+[*.fs]
+fsharp_record_multiline_formatter = number_of_items
+fsharp_max_record_number_of_items = 4
+"""
+
+ use configFixture =
+ new ConfigurationFile(defaultConfig, content = editorConfig)
+
+ use fsharpFile = new FSharpFile()
+
+ let config =
+ EditorConfig.readConfiguration fsharpFile.FSharpFile
+
+ config.MaxRecordNumberOfItems == 4
+
+ config.RecordMultilineFormatter == NumberOfItems
+
+[]
+let ``record character_width parsing test with single option`` () =
+ let editorConfig = """
+[*.fs]
+fsharp_max_record_width = 123
+"""
+
+ use configFixture =
+ new ConfigurationFile(defaultConfig, content = editorConfig)
+
+ use fsharpFile = new FSharpFile()
+
+ let config =
+ EditorConfig.readConfiguration fsharpFile.FSharpFile
+
+ config.MaxRecordWidth == 123
diff --git a/src/Fantomas.Tests/NumberOfItemsTests.fs b/src/Fantomas.Tests/NumberOfItemsListOrArrayTests.fs
similarity index 98%
rename from src/Fantomas.Tests/NumberOfItemsTests.fs
rename to src/Fantomas.Tests/NumberOfItemsListOrArrayTests.fs
index 6efe9bf42d..1bde9dd2f9 100644
--- a/src/Fantomas.Tests/NumberOfItemsTests.fs
+++ b/src/Fantomas.Tests/NumberOfItemsListOrArrayTests.fs
@@ -1,4 +1,4 @@
-module Fantomas.Tests.NumberOfItemsTests
+module Fantomas.Tests.NumberOfItemsListOrArrayTests
open NUnit.Framework
open FsUnit
@@ -284,7 +284,7 @@ let ``long expressions with number of items set to 3 will get split due to max l
"""
[]
-let ``character width with explicit width lists are formatted properly`` () =
+let ``character width with explicit width sized lists are formatted properly`` () =
formatSourceString false """
let x = [ a; b; c ]
let y = [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSoooooLong ]
diff --git a/src/Fantomas.Tests/NumberOfItemsRecordTests.fs b/src/Fantomas.Tests/NumberOfItemsRecordTests.fs
new file mode 100644
index 0000000000..cb0eb4945a
--- /dev/null
+++ b/src/Fantomas.Tests/NumberOfItemsRecordTests.fs
@@ -0,0 +1,987 @@
+module Fantomas.Tests.NumberOfItemsRecordTests
+
+open NUnit.Framework
+open FsUnit
+open Fantomas.Tests.TestHelper
+open Fantomas.FormatConfig
+
+let config =
+ { config with
+ RecordMultilineFormatter = NumberOfItems }
+
+[]
+let ``single member record stays on one line`` () =
+ formatSourceString false """let a = { Foo = "bar" }
+""" config
+ |> prepend newline
+ |> should equal """
+let a = { Foo = "bar" }
+"""
+
+[]
+let ``record instance`` () =
+ formatSourceString false """let myRecord =
+ { Level = 1
+ Progress = "foo"
+ Bar = "bar"
+ Street = "Bakerstreet"
+ Number = 42 }
+""" config
+ |> prepend newline
+ |> should equal """
+let myRecord =
+ { Level = 1
+ Progress = "foo"
+ Bar = "bar"
+ Street = "Bakerstreet"
+ Number = 42 }
+"""
+
+[]
+let ``nested record`` () =
+ formatSourceString false """let myRecord =
+ { Level = 1
+ Progress = "foo"
+ Bar = { Zeta = "bar" }
+ Address =
+ { Street = "Bakerstreet"
+ ZipCode = "9000" }
+ Number = 42 }
+""" config
+ |> prepend newline
+ |> should equal """
+let myRecord =
+ { Level = 1
+ Progress = "foo"
+ Bar = { Zeta = "bar" }
+ Address =
+ { Street = "Bakerstreet"
+ ZipCode = "9000" }
+ Number = 42 }
+"""
+
+[]
+let ``update record`` () =
+ formatSourceString false """let myRecord =
+ { myOldRecord
+ with Level = 2
+ Bar = "barry"
+ Progress = "fooey" }
+""" config
+ |> prepend newline
+ |> should equal """
+let myRecord =
+ { myOldRecord with
+ Level = 2
+ Bar = "barry"
+ Progress = "fooey" }
+"""
+
+[]
+let ``update record with single field`` () =
+ formatSourceString false """let myRecord =
+ { myOldRecord
+ with Level = 2 }
+""" config
+ |> prepend newline
+ |> should equal """
+let myRecord = { myOldRecord with Level = 2 }
+"""
+
+[]
+let ``record instance with inherit keyword`` () =
+ formatSourceString false """let a =
+ { inherit ProjectPropertiesBase<_>(projectTypeGuids, factoryGuid, targetFrameworkIds, dotNetCoreSDK)
+ buildSettings = FSharpBuildSettings()
+ targetPlatformData = targetPlatformData }
+""" config
+ |> prepend newline
+ |> should equal """
+let a =
+ { inherit ProjectPropertiesBase<_>(projectTypeGuids, factoryGuid, targetFrameworkIds, dotNetCoreSDK)
+ buildSettings = FSharpBuildSettings()
+ targetPlatformData = targetPlatformData }
+"""
+
+[]
+let ``record instance with inherit keyword and no fields`` () =
+ formatSourceString false """let a =
+ { inherit ProjectPropertiesBase<_>(projectTypeGuids, factoryGuid, targetFrameworkIds, dotNetCoreSDK) }
+""" config
+ |> prepend newline
+ |> should equal """
+let a =
+ { inherit ProjectPropertiesBase<_>(projectTypeGuids, factoryGuid, targetFrameworkIds, dotNetCoreSDK) }
+"""
+
+[]
+let ``type with record instance with inherit keyword`` () =
+ formatSourceString false """type ServerCannotBeResolvedException =
+ inherit CommunicationUnsuccessfulException
+
+ new(message) =
+ { inherit CommunicationUnsuccessfulException(message) }""" config
+ |> prepend newline
+ |> should equal """
+type ServerCannotBeResolvedException =
+ inherit CommunicationUnsuccessfulException
+
+ new(message) = { inherit CommunicationUnsuccessfulException(message) }
+"""
+
+[]
+let ``anonymous record`` () =
+ formatSourceString false """let meh =
+ {| Level = 1
+ Progress = "foo"
+ Bar = "bar"
+ Street = "Bakerstreet"
+ Number = 42 |}
+""" config
+ |> prepend newline
+ |> should equal """
+let meh =
+ {| Level = 1
+ Progress = "foo"
+ Bar = "bar"
+ Street = "Bakerstreet"
+ Number = 42 |}
+"""
+
+[]
+let ``anonymous record with single field update`` () =
+ formatSourceString false """let a = {| foo with Level = 7 |}
+""" config
+ |> prepend newline
+ |> should equal """
+let a = {| foo with Level = 7 |}
+"""
+
+[]
+let ``anonymous record with multiple field update`` () =
+ formatSourceString false """let a = {| foo with Level = 7; Square = 9 |}
+""" ({ config with MaxRecordWidth = 35 })
+ |> prepend newline
+ |> should equal """
+let a =
+ {| foo with
+ Level = 7
+ Square = 9 |}
+"""
+
+[]
+let ``anonymous type`` () =
+ formatSourceString false """type a = {| foo : string; bar : string |}
+""" config
+ |> prepend newline
+ |> should equal """
+type a =
+ {| foo: string
+ bar: string |}
+"""
+
+[]
+let ``anonymous record with single field`` () =
+ formatSourceString false """let a = {| A = "meh" |}
+""" config
+ |> prepend newline
+ |> should equal """
+let a = {| A = "meh" |}
+"""
+
+[]
+let ``anonymous record with child records`` () =
+ formatSourceString false """
+let anonRecord =
+ {| A = {| A1 = "string";A2LongerIdentifier = "foo" |};
+ B = {| B1 = 7 |}
+ C= { C1 = "foo"; C2LongerIdentifier = "bar"}
+ D = { D1 = "bar" } |}
+""" config
+ |> prepend newline
+ |> should equal """
+let anonRecord =
+ {| A =
+ {| A1 = "string"
+ A2LongerIdentifier = "foo" |}
+ B = {| B1 = 7 |}
+ C =
+ { C1 = "foo"
+ C2LongerIdentifier = "bar" }
+ D = { D1 = "bar" } |}
+"""
+
+[]
+let ``record as parameter to function`` () =
+ formatSourceString false """let configurations =
+ buildConfiguration { XXXXXXXXXXXX = "XXXXXXXXXXXXX"; YYYYYYYYYYYY = "YYYYYYYYYYYYYYY" }
+""" config
+ |> prepend newline
+ |> should equal """
+let configurations =
+ buildConfiguration
+ { XXXXXXXXXXXX = "XXXXXXXXXXXXX"
+ YYYYYYYYYYYY = "YYYYYYYYYYYYYYY" }
+"""
+
+[]
+let ``records in list`` () =
+ formatSourceString false """let configurations =
+ [
+ { Build = true; Configuration = "RELEASE"; Defines = ["FOO"] }
+ { Build = true; Configuration = "DEBUG"; Defines = ["FOO";"BAR"] }
+ { Build = true; Configuration = "UNKNOWN"; Defines = [] }
+ ]
+""" config
+ |> prepend newline
+ |> should equal """
+let configurations =
+ [ { Build = true
+ Configuration = "RELEASE"
+ Defines = [ "FOO" ] }
+ { Build = true
+ Configuration = "DEBUG"
+ Defines = [ "FOO"; "BAR" ] }
+ { Build = true
+ Configuration = "UNKNOWN"
+ Defines = [] } ]
+"""
+
+[]
+let ``anonymous records in list`` () =
+ formatSourceString false """let configurations =
+ [
+ {| Build = true; Configuration = "RELEASE"; Defines = ["FOO"] |}
+ {| Build = true; Configuration = "DEBUG"; Defines = ["FOO";"BAR"] |}
+ ]
+""" config
+ |> prepend newline
+ |> should equal """
+let configurations =
+ [ {| Build = true
+ Configuration = "RELEASE"
+ Defines = [ "FOO" ] |}
+ {| Build = true
+ Configuration = "DEBUG"
+ Defines = [ "FOO"; "BAR" ] |} ]
+"""
+
+[]
+let ``records in array`` () =
+ formatSourceString false """let configurations =
+ [|
+ { Build = true; Configuration = "RELEASE"; Defines = ["FOO"] }
+ { Build = true; Configuration = "DEBUG"; Defines = ["FOO";"BAR"] }
+ |]
+""" config
+ |> prepend newline
+ |> should equal """
+let configurations =
+ [| { Build = true
+ Configuration = "RELEASE"
+ Defines = [ "FOO" ] }
+ { Build = true
+ Configuration = "DEBUG"
+ Defines = [ "FOO"; "BAR" ] } |]
+"""
+
+[]
+let ``object expression`` () =
+ formatSourceString false """
+let obj1 = { new System.Object() with member x.ToString() = "F#" }
+""" config
+ |> prepend newline
+ |> should equal """
+let obj1 =
+ { new System.Object() with
+ member x.ToString() = "F#" }
+"""
+
+// FIXME: See https://github.com/fsprojects/fantomas/issues/1170
+[]
+[]
+let ``object expressions in list, 1170`` () =
+ formatSourceString false """
+let a =
+ [
+ { new System.Object() with member x.ToString() = "F#" }
+ { new System.Object() with member x.ToString() = "C#" }
+ ]
+""" config
+ |> prepend newline
+ |> should equal """
+let a =
+ [ { new System.Object() with
+ member x.ToString() = "F#" }
+ { new System.Object() with
+ member x.ToString() = "C#" } ]
+"""
+
+[]
+let ``record type signature with bracketOnSeparateLine`` () =
+ formatSourceString true """
+module RecordSignature
+/// Represents simple XML elements.
+type Element =
+ {
+ /// The attribute collection.
+ Attributes: IDictionary;
+
+ /// The children collection.
+ Children: seq;
+
+ /// The qualified name.
+ Name: Name }
+""" config
+ |> prepend newline
+ |> should equal """
+module RecordSignature
+/// Represents simple XML elements.
+type Element =
+ {
+ /// The attribute collection.
+ Attributes: IDictionary
+
+ /// The children collection.
+ Children: seq
+
+ /// The qualified name.
+ Name: Name }
+"""
+
+[]
+let ``record type with member definitions should align with bracket`` () =
+ formatSourceString false """
+type Range =
+ { From: float
+ To: float }
+ member this.Length = this.To - this.From
+"""
+ { config with
+ MaxValueBindingWidth = 120 }
+ |> prepend newline
+ |> should equal """
+type Range =
+ { From: float
+ To: float }
+ member this.Length = this.To - this.From
+"""
+
+[]
+let ``record type with interface`` () =
+ formatSourceString false """
+type MyRecord =
+ { SomeField : int
+ }
+ interface IMyInterface
+""" config
+ |> prepend newline
+ |> should equal """
+type MyRecord =
+ { SomeField: int }
+ interface IMyInterface
+"""
+
+[]
+[]
+let ``SynPat.Record in pattern match, 1173`` () =
+ formatSourceString false """match foo with
+| { Bar = bar; Level = 12; Vibes = plenty; Lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " } -> "7"
+| _ -> "8"
+""" config
+ |> prepend newline
+ |> should equal """
+match foo with
+| { Bar = bar
+ Level = 12
+ Vibes = plenty
+ Lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " } ->
+ "7"
+| _ -> "8"
+"""
+
+[]
+let ``record declaration`` () =
+ formatSourceString false """type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+""" config
+ |> prepend newline
+ |> should equal """
+type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+"""
+
+[]
+let ``record declaration in signature file`` () =
+ formatSourceString true """namespace X
+type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+""" config
+ |> prepend newline
+ |> should equal """
+namespace X
+
+type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+"""
+
+[]
+let ``record declaration with members in signature file`` () =
+ formatSourceString true """namespace X
+type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+ member Score : unit -> int
+""" config
+ |> prepend newline
+ |> should equal """
+namespace X
+
+type MyRecord =
+ { Level: int
+ Progress: string
+ Bar: string
+ Street: string
+ Number: int }
+ member Score: unit -> int
+"""
+
+
+[]
+let ``no newline before first multiline member`` () =
+ formatSourceString false """
+type ShortExpressionInfo =
+ { MaxWidth: int
+ StartColumn: int
+ ConfirmedMultiline: bool }
+ member x.IsTooLong maxPageWidth currentColumn =
+ currentColumn - x.StartColumn > x.MaxWidth // expression is not too long according to MaxWidth
+ || (currentColumn > maxPageWidth) // expression at current position is not going over the page width
+ member x.Foo() = ()
+"""
+ ({ config with
+ NewlineBetweenTypeDefinitionAndMembers = false })
+ |> prepend newline
+ |> should equal """
+type ShortExpressionInfo =
+ { MaxWidth: int
+ StartColumn: int
+ ConfirmedMultiline: bool }
+ member x.IsTooLong maxPageWidth currentColumn =
+ currentColumn - x.StartColumn > x.MaxWidth // expression is not too long according to MaxWidth
+ || (currentColumn > maxPageWidth) // expression at current position is not going over the page width
+
+ member x.Foo() = ()
+"""
+
+// FIXME: See https://github.com/fsprojects/fantomas/issues/1171
+[]
+[]
+let ``internal keyword before multiline record type, 1171`` () =
+ formatSourceString false """
+ type A = internal { ALongIdentifier: string; YetAnotherLongIdentifier: bool }""" config
+ |> prepend newline
+ |> should equal """
+type A =
+ internal
+ { ALongIdentifier: string
+ YetAnotherLongIdentifier: bool }
+"""
+
+// FIXME: See https://github.com/fsprojects/fantomas/issues/1171
+[]
+[]
+let ``internal keyword before multiline record type in signature file, 1171`` () =
+ formatSourceString true """namespace Bar
+
+ type A = internal { ALongIdentifier: string; YetAnotherLongIdentifier: bool }""" config
+ |> prepend newline
+ |> should equal """
+namespace Bar
+
+type A =
+ internal
+ { ALongIdentifier: string
+ YetAnotherLongIdentifier: bool }
+"""
+
+[]
+let ``indent update record fields far enough, 817`` () =
+ formatSourceString
+ false
+ "let expected = { ThisIsAThing.Empty with TheNewValue = 1; ThatValue = 2 }"
+ ({ config with IndentSize = 2 })
+ |> prepend newline
+ |> should equal """
+let expected =
+ { ThisIsAThing.Empty with
+ TheNewValue = 1
+ ThatValue = 2 }
+"""
+
+[]
+let ``indent update anonymous record fields far enough`` () =
+ formatSourceString
+ false
+ "let expected = {| ThisIsAThing.Empty with TheNewValue = 1; ThatValue = 2 |}"
+ ({ config with IndentSize = 2 })
+ |> prepend newline
+ |> should equal """
+let expected =
+ {| ThisIsAThing.Empty with
+ TheNewValue = 1
+ ThatValue = 2 |}
+"""
+
+[]
+let ``update record with standard indent`` () =
+ formatSourceString false "let expected = { ThisIsAThing.Empty with TheNewValue = 1; ThatValue = 2 }" config
+ |> prepend newline
+ |> should equal """
+let expected =
+ { ThisIsAThing.Empty with
+ TheNewValue = 1
+ ThatValue = 2 }
+"""
+
+[]
+let ``record type with attributes`` () =
+ formatSourceString false """
+[]
+type Args =
+ { []
+ []
+ []
+ Hi: int list }
+
+module Foo =
+
+ let r = 3
+""" config
+ |> prepend newline
+ |> should equal """
+[]
+type Args =
+ { []
+ []
+ []
+ Hi: int list }
+
+module Foo =
+
+ let r = 3
+"""
+
+[]
+let ``comment before access modifier of record type declaration`` () =
+ formatSourceString false """
+type TestType =
+ // Here is some comment about the type
+ // Some more comments
+ private
+ {
+ Foo : int
+ }
+""" config
+ |> prepend newline
+ |> should equal """
+type TestType =
+ // Here is some comment about the type
+ // Some more comments
+ private { Foo: int }
+"""
+
+[]
+let ``defines in record assignment, 968`` () =
+ formatSourceString false """
+let config = {
+ title = "Fantomas"
+ description = "Fantomas is a code formatter for F#"
+ theme_variant = Some "red"
+ root_url =
+ #if WATCH
+ "http://localhost:8080/"
+ #else
+ "https://fsprojects.github.io/fantomas/"
+ #endif
+}
+""" config
+ |> prepend newline
+ |> should equal """
+let config =
+ { title = "Fantomas"
+ description = "Fantomas is a code formatter for F#"
+ theme_variant = Some "red"
+ root_url =
+#if WATCH
+ "http://localhost:8080/"
+#else
+ "https://fsprojects.github.io/fantomas/"
+#endif
+ }
+"""
+
+// FIXME: See https://github.com/fsprojects/fantomas/issues/1172
+[]
+[]
+let ``comment after closing brace in nested record, 1172`` () =
+ formatSourceString false """
+let person =
+ { Name = "James"
+ Address = { Street = "Bakerstreet"; Number = 42 } // end address
+ } // end person
+""" config
+ |> prepend newline
+ |> should equal """
+let person =
+ { Name = "James"
+ Address =
+ { Street = "Bakerstreet"
+ Number = 42 } // end address
+ } // end person
+"""
+
+[]
+let ``number of items sized record definitions are formatted properly`` () =
+ formatSourceString false """
+type R = { a: int; b: string; c: float option }
+type S = { AReallyLongExpressionThatIsMuchLongerThan50Characters: int }
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems }
+ |> prepend newline
+ |> should equal """
+type R =
+ { a: int
+ b: string
+ c: float option }
+
+type S = { AReallyLongExpressionThatIsMuchLongerThan50Characters: int }
+"""
+
+[]
+let ``number of items sized record definitions with multiline block brackets on same column are formatted properly`` () =
+ formatSourceString false """
+type R = { a: int; b: string; c: float option }
+type S = { AReallyLongExpressionThatIsMuchLongerThan50Characters: int }
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems
+ MultilineBlockBracketsOnSameColumn = true }
+ |> prepend newline
+ |> should equal """
+type R =
+ {
+ a: int
+ b: string
+ c: float option
+ }
+
+type S = { AReallyLongExpressionThatIsMuchLongerThan50Characters: int }
+"""
+
+[]
+let ``number of items sized record expressions are formatted properly`` () =
+ formatSourceString false """
+let r = { a = x; b = y; z = c }
+let s = { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+let r' = { r with a = x; b = y; z = c }
+let s' = { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f r { a = x; b = y; z = c }
+g s { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f r' { r with a = x; b = y; z = c }
+g s' { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems }
+ |> prepend newline
+ |> should equal """
+let r =
+ { a = x
+ b = y
+ z = c }
+
+let s =
+ { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+let r' =
+ { r with
+ a = x
+ b = y
+ z = c }
+
+let s' =
+ { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f
+ r
+ { a = x
+ b = y
+ z = c }
+
+g s { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f
+ r'
+ { r with
+ a = x
+ b = y
+ z = c }
+
+g s' { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+"""
+
+[]
+let ``number of items sized record expressions with multiline block brackets on same column are formatted properly`` () =
+ formatSourceString false """
+let r = { a = x; b = y; z = c }
+let s = { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+let r' = { r with a = x; b = y; z = c }
+let s' = { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f r { a = x; b = y; z = c }
+g s { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f r' { r with a = x; b = y; z = c }
+g s' { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems
+ MultilineBlockBracketsOnSameColumn = true }
+ |> prepend newline
+ |> should equal """
+let r =
+ {
+ a = x
+ b = y
+ z = c
+ }
+
+let s =
+ { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+let r' =
+ { r with
+ a = x
+ b = y
+ z = c
+ }
+
+let s' =
+ { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f
+ r
+ {
+ a = x
+ b = y
+ z = c
+ }
+
+g s { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+
+f
+ r'
+ { r with
+ a = x
+ b = y
+ z = c
+ }
+
+g s' { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 }
+"""
+
+[]
+let ``number of items sized anonymous record expressions are formatted properly`` () =
+ formatSourceString false """
+let r = {| a = x; b = y; z = c |}
+let s = {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+let r' = {| r with a = x; b = y; z = c |}
+let s' = {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f r {| a = x; b = y; z = c |}
+g s {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f r' {| r with a = x; b = y; z = c |}
+g s' {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems }
+ |> prepend newline
+ |> should equal """
+let r =
+ {| a = x
+ b = y
+ z = c |}
+
+let s =
+ {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+let r' =
+ {| r with
+ a = x
+ b = y
+ z = c |}
+
+let s' =
+ {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f
+ r
+ {| a = x
+ b = y
+ z = c |}
+
+g s {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f
+ r'
+ {| r with
+ a = x
+ b = y
+ z = c |}
+
+g s' {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+"""
+
+[]
+let ``number of items sized anonymous record expressions with multiline block brackets on same column are formatted properly`` () =
+ formatSourceString false """
+let r = {| a = x; b = y; z = c |}
+let s = {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+let r' = {| r with a = x; b = y; z = c |}
+let s' = {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f r {| a = x; b = y; z = c |}
+g s {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f r' {| r with a = x; b = y; z = c |}
+g s' {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+ """
+ { config with
+ RecordMultilineFormatter = NumberOfItems
+ MultilineBlockBracketsOnSameColumn = true }
+ |> prepend newline
+ |> should equal """
+let r =
+ {|
+ a = x
+ b = y
+ z = c
+ |}
+
+let s =
+ {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+let r' =
+ {| r with
+ a = x
+ b = y
+ z = c
+ |}
+
+let s' =
+ {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f
+ r
+ {|
+ a = x
+ b = y
+ z = c
+ |}
+
+g s {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+
+f
+ r'
+ {| r with
+ a = x
+ b = y
+ z = c
+ |}
+
+g s' {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |}
+"""
+
+[]
+let ``number of items sized anonymous record types are formatted properly`` () =
+ formatSourceString false """
+let f (x: {| x: int; y: obj |}) = x
+let g (x: {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}) = x
+type A = {| x: int; y: obj |}
+type B = {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}
+"""
+ { config with
+ RecordMultilineFormatter = NumberOfItems }
+ |> prepend newline
+ |> should equal """
+let f (x: {| x: int
+ y: obj |}) =
+ x
+
+let g (x: {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}) = x
+
+type A =
+ {| x: int
+ y: obj |}
+
+type B = {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}
+"""
+
+// FIXME: See https://github.com/fsprojects/fantomas/issues/1167
+[]
+[]
+let ``number of items sized anonymous record types with multiline block brackets on same column are formatted properly`` () =
+ formatSourceString false """
+let f (x: {| x: int; y: obj |}) = x
+let g (x: {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}) = x
+type A = {| x: int; y: obj |}
+type B = {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}
+"""
+ { config with
+ RecordMultilineFormatter = NumberOfItems
+ MultilineBlockBracketsOnSameColumn = true }
+ |> prepend newline
+ |> should equal """
+
+let f (x: {|
+ x : int
+ y : AReallyLongTypeThatIsMuchLongerThan40Characters
+ |}) =
+ x
+let g (x: {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}) = x
+
+type A =
+ {|
+ x: int
+ y: AReallyLongTypeThatIsMuchLongerThan40Characters
+ |}
+
+type B = {| x: AReallyLongTypeThatIsMuchLongerThan40Characters |}
+"""
diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs
index 9dc83db418..f2fed9953c 100644
--- a/src/Fantomas/CodePrinter.fs
+++ b/src/Fantomas/CodePrinter.fs
@@ -1417,7 +1417,7 @@ and genExpr astContext synExpr =
isSmallExpression size smallExpression multilineExpression ctx
| Record (inheritOpt, xs, eo) ->
- let shortRecordExpr =
+ let smallRecordExpr =
sepOpenS
+> leaveLeftBrace synExpr.Range
+> optSingle (fun (inheritType, inheritExpr) ->
@@ -1435,10 +1435,12 @@ and genExpr astContext synExpr =
(genMultilineRecordInstanceAlignBrackets inheritOpt xs eo synExpr astContext)
(genMultilineRecordInstance inheritOpt xs eo synExpr astContext)
- fun ctx -> isShortExpression ctx.Config.MaxRecordWidth shortRecordExpr multilineRecordExpr ctx
+ fun ctx ->
+ let size = getRecordSize ctx xs
+ isSmallExpression size smallRecordExpr multilineRecordExpr ctx
| AnonRecord (isStruct, fields, copyInfo) ->
- let shortExpression =
+ let smallExpression =
onlyIf isStruct !- "struct "
+> sepOpenAnonRecd
+> optSingle (fun e -> genExpr astContext e +> !- " with ") copyInfo
@@ -1450,7 +1452,9 @@ and genExpr astContext synExpr =
(genMultilineAnonRecordAlignBrackets isStruct fields copyInfo astContext)
(genMultilineAnonRecord isStruct fields copyInfo astContext)
- fun (ctx: Context) -> isShortExpression ctx.Config.MaxRecordWidth shortExpression longExpression ctx
+ fun (ctx: Context) ->
+ let size = getRecordSize ctx fields
+ isSmallExpression size smallExpression longExpression ctx
| ObjExpr (t, eio, bd, ims, range) ->
ifAlignBrackets
@@ -3093,7 +3097,7 @@ and genTypeDefn astContext (TypeDef (ats, px, ao, tds, tcs, tdr, ms, s, preferPo
+> unindent
| Simple (TDSRRecord (ao', fs)) ->
- let shortExpression =
+ let smallExpression =
sepSpace
+> optSingle (fun ao -> genAccess ao +> sepSpace) ao'
+> sepOpenS
@@ -3108,11 +3112,13 @@ and genTypeDefn astContext (TypeDef (ats, px, ao, tds, tcs, tdr, ms, s, preferPo
(genMultilineSimpleRecordTypeDefn tdr ms ao' fs astContext)
let bodyExpr ctx =
+ let size = getRecordSize ctx fs
+
if (List.isEmpty ms) then
- (isShortExpression
- ctx.Config.MaxRecordWidth
+ (isSmallExpression
+ size
(enterNodeFor SynTypeDefnSimpleRepr_Record tdr.Range
- +> shortExpression)
+ +> smallExpression)
multilineExpression
+> leaveNodeFor SynTypeDefnSimpleRepr_Record tdr.Range // this will only print something when there is trivia after } in the short expression
// Yet it cannot be part of the short expression otherwise the multiline expression would be triggered unwillingly.
@@ -3371,7 +3377,6 @@ and genSigTypeDefn astContext (SigTypeDef (ats, px, ao, tds, tcs, tdr, ms, s, pr
+> unindent
| SigSimple (TDSRUnion (ao', xs)) ->
-
let unionCases =
match xs with
| [] -> id
@@ -3411,7 +3416,7 @@ and genSigTypeDefn astContext (SigTypeDef (ats, px, ao, tds, tcs, tdr, ms, s, pr
+> unindent
| SigSimple (TDSRRecord (ao', fs)) ->
- let shortExpr =
+ let smallExpr =
typeName
+> sepEq
+> sepSpace
@@ -3427,9 +3432,11 @@ and genSigTypeDefn astContext (SigTypeDef (ats, px, ao, tds, tcs, tdr, ms, s, pr
(genSigSimpleRecord typeName tdr ms ao' fs astContext)
fun ctx ->
+ let size = getRecordSize ctx fs
+
if List.isNotEmpty ms
then longExpr ctx
- else isShortExpression ctx.Config.MaxRecordWidth shortExpr longExpr ctx
+ else isSmallExpression size smallExpr longExpr ctx
| SigSimple TDSRNone ->
let genMembers =
@@ -3749,7 +3756,7 @@ and genType astContext outerBracket t =
&& astContext.IsCStylePattern) (genTypeByLookup astContext t) (!-s)
|> genTriviaFor Ident_ current.Range
| TAnonRecord (isStruct, fields) ->
- let shortExpression =
+ let smallExpression =
ifElse isStruct !- "struct " sepNone
+> sepOpenAnonRecd
+> col sepSemi fields (genAnonRecordFieldType astContext)
@@ -3761,7 +3768,9 @@ and genType astContext outerBracket t =
+> atCurrentColumn (col sepSemiNln fields (genAnonRecordFieldType astContext))
+> sepCloseAnonRecd
- fun (ctx: Context) -> isShortExpression ctx.Config.MaxRecordWidth shortExpression longExpression ctx
+ fun (ctx: Context) ->
+ let size = getRecordSize ctx fields
+ isSmallExpression size smallExpression longExpression ctx
| TParen (innerT) ->
sepOpenT
+> loop innerT
diff --git a/src/Fantomas/Context.fs b/src/Fantomas/Context.fs
index 48752a5ada..e9478afbba 100644
--- a/src/Fantomas/Context.fs
+++ b/src/Fantomas/Context.fs
@@ -527,6 +527,11 @@ let internal getListOrArrayExprSize ctx maxWidth xs =
| MultilineFormatterType.CharacterWidth -> Size.CharacterWidth maxWidth
| MultilineFormatterType.NumberOfItems -> Size.NumberOfItems(List.length xs, ctx.Config.MaxArrayOrListNumberOfItems)
+let internal getRecordSize ctx fields =
+ match ctx.Config.RecordMultilineFormatter with
+ | MultilineFormatterType.CharacterWidth -> Size.CharacterWidth ctx.Config.MaxRecordWidth
+ | MultilineFormatterType.NumberOfItems -> Size.NumberOfItems(List.length fields, ctx.Config.MaxRecordNumberOfItems)
+
/// b is true, apply f1 otherwise apply f2
let internal ifElse b (f1: Context -> Context) f2 (ctx: Context) = if b then f1 ctx else f2 ctx
diff --git a/src/Fantomas/FormatConfig.fs b/src/Fantomas/FormatConfig.fs
index 8e07b59c47..cc99c2b31b 100644
--- a/src/Fantomas/FormatConfig.fs
+++ b/src/Fantomas/FormatConfig.fs
@@ -44,6 +44,8 @@ type FormatConfig =
MaxIfThenElseShortWidth: Num
MaxInfixOperatorExpression: Num
MaxRecordWidth: Num
+ MaxRecordNumberOfItems: Num
+ RecordMultilineFormatter: MultilineFormatterType
MaxArrayOrListWidth: Num
MaxArrayOrListNumberOfItems: Num
ArrayOrListMultilineFormatter: MultilineFormatterType
@@ -78,6 +80,8 @@ type FormatConfig =
MaxIfThenElseShortWidth = 40
MaxInfixOperatorExpression = 50
MaxRecordWidth = 40
+ MaxRecordNumberOfItems = 1
+ RecordMultilineFormatter = CharacterWidth
MaxArrayOrListWidth = 40
MaxArrayOrListNumberOfItems = 1
ArrayOrListMultilineFormatter = CharacterWidth