diff --git a/docs/Documentation.md b/docs/Documentation.md index efc5e8766c..05e42b6f69 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -93,6 +93,7 @@ fsharp_alternative_long_member_definitions=false fsharp_multi_line_lambda_closing_newline=false fsharp_disable_elmish_syntax=false fsharp_keep_indent_in_branch=false +fsharp_blank_lines_around_nested_multiline_expressions=true fsharp_strict_mode=false ``` @@ -1060,6 +1061,48 @@ let main argv = 0 ``` +### fsharp_blank_lines_around_nested_multiline_expressions + +Surround **nested** multi-line expressions with blank lines. +Existing blank lines are always preserved (via trivia). +Top level expressions will always follow the [2020 blank lines revision](https://github.com/fsprojects/fantomas/blob/master/docs/FormattingConventions.md#2020-revision) principle. +Default = true. + +`defaultConfig` + +```fsharp +let topLevelFunction () = + printfn "Something to print" + + try + nothing () + with + | ex -> + splash () + () + +let secondTopLevelFunction () = + // ... + () +``` + +`{ defaultConfig with BlankLinesAroundNestedMultilineExpressions = false }` + +```fsharp +let topLevelFunction () = + printfn "Something to print" + try + nothing () + with + | ex -> + splash () + () + +let secondTopLevelFunction () = + // ... + () +``` + ### fsharp_strict_mode If being set, pretty printing is only done via ASTs. Compiler directives, inline comments and block comments will be ignored. diff --git a/src/Fantomas.Tests/BlankLinesAroundNestedMultilineExpressions.fs b/src/Fantomas.Tests/BlankLinesAroundNestedMultilineExpressions.fs new file mode 100644 index 0000000000..ba1c9f99d6 --- /dev/null +++ b/src/Fantomas.Tests/BlankLinesAroundNestedMultilineExpressions.fs @@ -0,0 +1,245 @@ +module Fantomas.Tests.BlankLinesAroundNestedMultilineExpressions + +open NUnit.Framework +open FsUnit +open Fantomas.Tests.TestHelper + +let config = + { config with + BlankLinesAroundNestedMultilineExpressions = false } + +[] +let ``basic behavior`` () = + formatSourceString + false + """ +let topLevelFunction () = + let innerValue = 23 + let innerMultilineFunction () = + // some comment + printfn "foo" + () +""" + config + |> prepend newline + |> should + equal + """ +let topLevelFunction () = + let innerValue = 23 + let innerMultilineFunction () = + // some comment + printfn "foo" + () +""" + +[] +let ``existing newlines are preserved`` () = + formatSourceString + false + """ +let topLevelFunction () = + let innerValue = 23 + + let innerMultilineFunction () = + // some comment + printfn "foo" + () +""" + config + |> prepend newline + |> should + equal + """ +let topLevelFunction () = + let innerValue = 23 + + let innerMultilineFunction () = + // some comment + printfn "foo" + () +""" + +[] +let ``with sequential expressions`` () = + formatSourceString + false + """ +let topLevelFunction () = + printfn "Something to print" + try + nothing () + with + | ex -> + splash () + () +""" + config + |> prepend newline + |> should + equal + """ +let topLevelFunction () = + printfn "Something to print" + try + nothing () + with ex -> splash () + () +""" + +[] +let ``disable blank lines around multiline expressions inside let binding, 1370`` () = + formatSourceString + false + """ +let emit nodes = + let an = AssemblyName("a") + let ab = + AppDomain.CurrentDomain.DefineDynamicAssembly( + an, + AssemblyBuilderAccess.RunAndCollect + ) + let mb = ab.DefineDynamicModule("a") + let tb = mb.DefineType("Program") + nodes +""" + config + |> prepend newline + |> should + equal + """ +let emit nodes = + let an = AssemblyName("a") + let ab = + AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.RunAndCollect) + let mb = ab.DefineDynamicModule("a") + let tb = mb.DefineType("Program") + nodes +""" + +[] +let ``disable blank lines inside type constructor`` () = + formatSourceString + false + """ +type MNIST(path:string, ?urls:seq, ?train:bool, ?transform:Tensor->Tensor, ?targetTransform:Tensor->Tensor) = + inherit Dataset() + let path = Path.Combine(path, "mnist") |> Path.GetFullPath + let train = defaultArg train true + let transform = defaultArg transform (fun t -> (t - 0.1307) / 0.3081) + let targetTransform = defaultArg targetTransform id + let urls = List.ofSeq <| defaultArg urls (Seq.ofList + ["http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz"; + "http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz"; + "http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz"; + "http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz"]) + let files = [for url in urls do Path.Combine(path, Path.GetFileName(url))] + let filesProcessed = [for file in files do Path.ChangeExtension(file, ".tensor")] + let data, target = + Directory.CreateDirectory(path) |> ignore + let mutable data = dsharp.zero() + let mutable target = dsharp.zero() + if train then + if not (File.Exists(files.[0])) then download urls.[0] files.[0] + if not (File.Exists(files.[1])) then download urls.[1] files.[1] + if File.Exists(filesProcessed.[0]) then data <- dsharp.load(filesProcessed.[0]) else data <- MNIST.LoadMNISTImages(files.[0]); dsharp.save(data, filesProcessed.[0]) + if File.Exists(filesProcessed.[1]) then target <- dsharp.load(filesProcessed.[1]) else target <- MNIST.LoadMNISTLabels(files.[1]); dsharp.save(target, filesProcessed.[1]) + else + if not (File.Exists(files.[2])) then download urls.[2] files.[2] + if not (File.Exists(files.[3])) then download urls.[3] files.[3] + if File.Exists(filesProcessed.[2]) then data <- dsharp.load(filesProcessed.[2]) else data <- MNIST.LoadMNISTImages(files.[2]); dsharp.save(data, filesProcessed.[2]) + if File.Exists(filesProcessed.[3]) then target <- dsharp.load(filesProcessed.[3]) else target <- MNIST.LoadMNISTLabels(files.[3]); dsharp.save(target, filesProcessed.[3]) + data, target +""" + config + |> prepend newline + |> should + equal + """ +type MNIST + ( + path: string, + ?urls: seq, + ?train: bool, + ?transform: Tensor -> Tensor, + ?targetTransform: Tensor -> Tensor + ) = + inherit Dataset() + let path = + Path.Combine(path, "mnist") |> Path.GetFullPath + let train = defaultArg train true + let transform = + defaultArg transform (fun t -> (t - 0.1307) / 0.3081) + let targetTransform = defaultArg targetTransform id + let urls = + List.ofSeq + <| defaultArg + urls + (Seq.ofList [ "http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz" + "http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz" + "http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz" + "http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz" ]) + let files = + [ for url in urls do + Path.Combine(path, Path.GetFileName(url)) ] + let filesProcessed = + [ for file in files do + Path.ChangeExtension(file, ".tensor") ] + let data, target = + Directory.CreateDirectory(path) |> ignore + let mutable data = dsharp.zero () + let mutable target = dsharp.zero () + if train then + if not (File.Exists(files.[0])) then + download urls.[0] files.[0] + if not (File.Exists(files.[1])) then + download urls.[1] files.[1] + if File.Exists(filesProcessed.[0]) then + data <- dsharp.load (filesProcessed.[0]) + else + data <- MNIST.LoadMNISTImages(files.[0]) + dsharp.save (data, filesProcessed.[0]) + if File.Exists(filesProcessed.[1]) then + target <- dsharp.load (filesProcessed.[1]) + else + target <- MNIST.LoadMNISTLabels(files.[1]) + dsharp.save (target, filesProcessed.[1]) + else + if not (File.Exists(files.[2])) then + download urls.[2] files.[2] + if not (File.Exists(files.[3])) then + download urls.[3] files.[3] + if File.Exists(filesProcessed.[2]) then + data <- dsharp.load (filesProcessed.[2]) + else + data <- MNIST.LoadMNISTImages(files.[2]) + dsharp.save (data, filesProcessed.[2]) + if File.Exists(filesProcessed.[3]) then + target <- dsharp.load (filesProcessed.[3]) + else + target <- MNIST.LoadMNISTLabels(files.[3]) + dsharp.save (target, filesProcessed.[3]) + data, target +""" + +[] +let ``computation expressions`` () = + formatSourceString + false + """ +let comp = + eventually { for x in 1 .. 2 do + printfn " x = %d" x + return 3 + 4 }""" + config + |> prepend newline + |> should + equal + """ +let comp = + eventually { + for x in 1 .. 2 do + printfn " x = %d" x + return 3 + 4 + } +""" diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index c6242bc4ff..cd0e07a0bf 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -85,6 +85,7 @@ + diff --git a/src/Fantomas.Tests/KeepIndentInBranch.fs b/src/Fantomas.Tests/KeepIndentInBranch.fs index f98c292bab..7b89880f2d 100644 --- a/src/Fantomas.Tests/KeepIndentInBranch.fs +++ b/src/Fantomas.Tests/KeepIndentInBranch.fs @@ -859,3 +859,51 @@ module Foo = Thing.execute bar baz (thing, instructions) 0 """ + +[] +let ``in combination with NewlinesAroundInnerMultilineExpressions`` () = + formatSourceString + false + """ +module Foo = + let main (args: _) = + let thing1 = () + printfn "" + if hasInstructions () then + printfn "" + 2 + else + log.LogInformation("") + match Something.foo args with + | DryRunMode.Dry -> + printfn "" + 0 + | DryRunMode.Wet -> + Thing.execute bar baz (thing, instructions) + 0 +""" + { config with + BlankLinesAroundNestedMultilineExpressions = false } + |> prepend newline + |> should + equal + """ +module Foo = + let main (args: _) = + let thing1 = () + printfn "" + if hasInstructions () then + printfn "" + 2 + else + + log.LogInformation("") + match Something.foo args with + | DryRunMode.Dry -> + printfn "" + 0 + | DryRunMode.Wet -> + + Thing.execute bar baz (thing, instructions) + 0 +""" diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs index 9af4d30a52..a67cb4d4dd 100644 --- a/src/Fantomas/CodePrinter.fs +++ b/src/Fantomas/CodePrinter.fs @@ -1467,7 +1467,7 @@ and genExpr astContext synExpr ctx = let r = getRangeOfCompExprStatement ces let sepNln = getSepNln ces r ColMultilineItem(expr, sepNln, r)) - |> colWithNlnWhenItemIsMultiline + |> colWithNlnWhenItemIsMultilineUsingConfig | ArrayOrListOfSeqExpr (isArray, e) as alNode -> let astContext = { astContext with IsNakedRange = true } @@ -2083,7 +2083,8 @@ and genExpr astContext synExpr ctx = ) ] let items = letBindings bs @ synExpr e - atCurrentColumn (colWithNlnWhenItemIsMultiline items) ctx + + atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) ctx // Could customize a bit if e is single line | TryWith (e, cs) -> @@ -2160,7 +2161,7 @@ and genExpr astContext synExpr ctx = ColMultilineItem(expr, sepNln, r)) - atCurrentColumn (colWithNlnWhenItemIsMultiline items) + atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) // A generalization of IfThenElse | ElIf ((e1, e2, _, _, _) :: es, enOpt) -> @@ -4528,7 +4529,7 @@ and genMemberDefnList astContext nodes = :: (collectItems rest) collectItems nodes - |> colWithNlnWhenItemIsMultiline + |> colWithNlnWhenItemIsMultilineUsingConfig and genMemberDefn astContext node = match node with @@ -5316,7 +5317,7 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> e2.Range ) ] - colWithNlnWhenItemIsMultiline items + colWithNlnWhenItemIsMultilineUsingConfig items | LetOrUses (bs, (KeepIndentMatch (me, clauses, matchRange, matchTriviaType) as em)) -> let bs = List.map @@ -5341,8 +5342,8 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> em.Range ) - colWithNlnWhenItemIsMultiline [ yield! bs - yield m ] + colWithNlnWhenItemIsMultilineUsingConfig [ yield! bs + yield m ] | LetOrUses (bs, Sequential (e1, KeepIndentMatch (me, clauses, matchRange, matchTriviaType), true)) -> let bs = List.map @@ -5373,9 +5374,9 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> matchRange ) - colWithNlnWhenItemIsMultiline [ yield! bs - yield e1 - yield m ] + colWithNlnWhenItemIsMultilineUsingConfig [ yield! bs + yield e1 + yield m ] | KeepIndentMatch (me, clauses, matchRange, matchTriviaType) -> genKeepIndentMatch astContext me clauses matchRange matchTriviaType | Sequential (e1, (KeepIndentIfThenElse (branches, elseBranch, ifElseRange) as e2), true) -> @@ -5391,7 +5392,7 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> e2.Range ) ] - colWithNlnWhenItemIsMultiline items + colWithNlnWhenItemIsMultilineUsingConfig items | LetOrUses (bs, (KeepIndentIfThenElse (branches, elseBranch, ifElseRange))) -> let bs = List.map @@ -5416,8 +5417,8 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> ifElseRange ) - colWithNlnWhenItemIsMultiline [ yield! bs - yield ifElse ] + colWithNlnWhenItemIsMultilineUsingConfig [ yield! bs + yield ifElse ] | LetOrUses (bs, Sequential (e1, KeepIndentIfThenElse (branches, elseBranch, ifElseRange), true)) -> let bs = List.map @@ -5448,9 +5449,9 @@ and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> ifElseRange ) - colWithNlnWhenItemIsMultiline [ yield! bs - yield e1 - yield ifElse ] + colWithNlnWhenItemIsMultilineUsingConfig [ yield! bs + yield e1 + yield ifElse ] | KeepIndentIfThenElse (branches, elseBranch, ifElseRange) -> genKeepIdentIf astContext branches elseBranch ifElseRange | _ -> genExpr astContext e diff --git a/src/Fantomas/Context.fs b/src/Fantomas/Context.fs index 3e4d3dbf0d..3f6f5ba6cf 100644 --- a/src/Fantomas/Context.fs +++ b/src/Fantomas/Context.fs @@ -1507,6 +1507,12 @@ let internal colWithNlnWhenItemIsMultiline (items: ColMultilineItem list) = impl items +let internal colWithNlnWhenItemIsMultilineUsingConfig (items: ColMultilineItem list) (ctx: Context) = + if ctx.Config.BlankLinesAroundNestedMultilineExpressions then + colWithNlnWhenItemIsMultiline items ctx + else + col sepNln items (fun (ColMultilineItem (expr, _, _)) -> expr) ctx + let internal genTriviaBeforeClausePipe (rangeOfClause: Range) ctx = (Map.tryFindOrEmptyList BAR ctx.TriviaTokenNodes) |> List.tryFind diff --git a/src/Fantomas/FormatConfig.fs b/src/Fantomas/FormatConfig.fs index 822f71868e..d81519118c 100644 --- a/src/Fantomas/FormatConfig.fs +++ b/src/Fantomas/FormatConfig.fs @@ -94,6 +94,7 @@ type FormatConfig = DisableElmishSyntax: bool EndOfLine: EndOfLineStyle KeepIndentInBranch: bool + BlankLinesAroundNestedMultilineExpressions: bool /// Pretty printing based on ASTs only StrictMode: bool } @@ -134,4 +135,5 @@ type FormatConfig = DisableElmishSyntax = false EndOfLine = EndOfLineStyle.FromEnvironment KeepIndentInBranch = false + BlankLinesAroundNestedMultilineExpressions = true StrictMode = false }