From bd0ba0e7ad148505d069ef850a39cee405677f80 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Mon, 27 Sep 2021 15:08:57 +0200 Subject: [PATCH] Update records indent from the curly brace (#1892) * Update records indent from the curly brace. Fixes #1876. * WIP anonymous records * Indent multiline record field expressions from the opening brace. * Restore correct indent after update record expression. * trigger CI --- src/Fantomas.Tests/AppTests.fs | 3 +- src/Fantomas.Tests/CommentTests.fs | 47 ++ src/Fantomas.Tests/CompilerDirectivesTests.fs | 8 +- src/Fantomas.Tests/LambdaTests.fs | 4 +- .../NumberOfItemsRecordTests.fs | 54 +- src/Fantomas.Tests/OperatorTests.fs | 8 +- src/Fantomas.Tests/PatternMatchingTests.fs | 11 +- src/Fantomas.Tests/RecordTests.fs | 502 +++++++++++++++--- src/Fantomas/CodePrinter.fs | 175 +++--- src/Fantomas/Context.fs | 26 +- 10 files changed, 651 insertions(+), 187 deletions(-) diff --git a/src/Fantomas.Tests/AppTests.fs b/src/Fantomas.Tests/AppTests.fs index 2b73d845aa..f6f07d54b6 100644 --- a/src/Fantomas.Tests/AppTests.fs +++ b/src/Fantomas.Tests/AppTests.fs @@ -436,8 +436,7 @@ let ``classes and private implicit constructors`` () = do self.PrintMessage() member this.PrintMessage() = printf \"Creating MyClass2 with Data %d\" data\"\"\" - { config with - MaxFunctionBindingWidth = 120 } + { config with MaxFunctionBindingWidth = 120 } " [] diff --git a/src/Fantomas.Tests/CommentTests.fs b/src/Fantomas.Tests/CommentTests.fs index ffb1aeb626..6fd455434f 100644 --- a/src/Fantomas.Tests/CommentTests.fs +++ b/src/Fantomas.Tests/CommentTests.fs @@ -289,6 +289,53 @@ let a = B = 7 } """ +[] +let ``comment alignment above record field`` () = + formatSourceString + false + """ +let a = + { c = 4 + // foo + // bar + B = 7 } +""" + config + |> prepend newline + |> should + equal + """ +let a = + { c = 4 + // foo + // bar + B = 7 } +""" + +[] +let ``comment alignment above record field, fsharp_space_around_delimiter = false`` () = + formatSourceString + false + """ +let a = + { c = 4 + // foo + // bar + B = 7 } +""" + { config with + SpaceAroundDelimiter = false } + |> prepend newline + |> should + equal + """ +let a = + {c = 4 + // foo + // bar + B = 7} +""" + [] let ``shouldn't break on one-line comment`` () = formatSourceString diff --git a/src/Fantomas.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Tests/CompilerDirectivesTests.fs index 965f290d94..e0d126a17d 100644 --- a/src/Fantomas.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Tests/CompilerDirectivesTests.fs @@ -1911,7 +1911,7 @@ let config = #if WATCH #else - "https://fsprojects.github.io/fantomas/" + "https://fsprojects.github.io/fantomas/" #endif } """ @@ -1944,7 +1944,7 @@ let config = theme_variant = Some "red" root_url = #if WATCH - "http://localhost:8080/" + "http://localhost:8080/" #else #endif @@ -1979,9 +1979,9 @@ let config = theme_variant = Some "red" root_url = #if WATCH - "http://localhost:8080/" + "http://localhost:8080/" #else - "https://fsprojects.github.io/fantomas/" + "https://fsprojects.github.io/fantomas/" #endif } """ diff --git a/src/Fantomas.Tests/LambdaTests.fs b/src/Fantomas.Tests/LambdaTests.fs index f83158d177..f47fe6754a 100644 --- a/src/Fantomas.Tests/LambdaTests.fs +++ b/src/Fantomas.Tests/LambdaTests.fs @@ -933,8 +933,8 @@ module Foo = module Foo = let bar () = { Foo = - blah - |> Struct.map (fun _ (a, _, _) -> filterBackings a) } + blah + |> Struct.map (fun _ (a, _, _) -> filterBackings a) } """ [] diff --git a/src/Fantomas.Tests/NumberOfItemsRecordTests.fs b/src/Fantomas.Tests/NumberOfItemsRecordTests.fs index 151b120adc..786bc5cd46 100644 --- a/src/Fantomas.Tests/NumberOfItemsRecordTests.fs +++ b/src/Fantomas.Tests/NumberOfItemsRecordTests.fs @@ -70,8 +70,8 @@ let myRecord = Progress = "foo" Bar = { Zeta = "bar" } Address = - { Street = "Bakerstreet" - ZipCode = "9000" } + { Street = "Bakerstreet" + ZipCode = "9000" } Number = 42 } """ @@ -92,9 +92,9 @@ let ``update record`` () = """ let myRecord = { myOldRecord with - Level = 2 - Bar = "barry" - Progress = "fooey" } + Level = 2 + Bar = "barry" + Progress = "fooey" } """ [] @@ -220,8 +220,8 @@ let ``anonymous record with multiple field update`` () = """ let a = {| foo with - Level = 7 - Square = 9 |} + Level = 7 + Square = 9 |} """ [] @@ -272,12 +272,12 @@ let anonRecord = """ let anonRecord = {| A = - {| A1 = "string" - A2LongerIdentifier = "foo" |} + {| A1 = "string" + A2LongerIdentifier = "foo" |} B = {| B1 = 7 |} C = - { C1 = "foo" - C2LongerIdentifier = "bar" } + { C1 = "foo" + C2LongerIdentifier = "bar" } D = { D1 = "bar" } |} """ @@ -708,8 +708,8 @@ let ``update record with standard indent`` () = """ let expected = { ThisIsAThing.Empty with - TheNewValue = 1 - ThatValue = 2 } + TheNewValue = 1 + ThatValue = 2 } """ [] @@ -797,9 +797,9 @@ let config = theme_variant = Some "red" root_url = #if WATCH - "http://localhost:8080/" + "http://localhost:8080/" #else - "https://fsprojects.github.io/fantomas/" + "https://fsprojects.github.io/fantomas/" #endif } """ @@ -911,9 +911,9 @@ let s = let r' = { r with - a = x - b = y - z = c } + a = x + b = y + z = c } let s' = { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 } @@ -929,9 +929,9 @@ g s { AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 } f r' { r with - a = x - b = y - z = c } + a = x + b = y + z = c } g s' { s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 } """ @@ -1036,9 +1036,9 @@ let s = let r' = {| r with - a = x - b = y - z = c |} + a = x + b = y + z = c |} let s' = {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |} @@ -1054,9 +1054,9 @@ g s {| AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |} f r' {| r with - a = x - b = y - z = c |} + a = x + b = y + z = c |} g s' {| s with AReallyLongExpressionThatIsMuchLongerThan50Characters = 1 |} """ diff --git a/src/Fantomas.Tests/OperatorTests.fs b/src/Fantomas.Tests/OperatorTests.fs index 600c6ffe43..d4db9ca4b0 100644 --- a/src/Fantomas.Tests/OperatorTests.fs +++ b/src/Fantomas.Tests/OperatorTests.fs @@ -766,11 +766,11 @@ Fooey " let r = {| Foo = - a - && // && b - c + a + && // && b + c Bar = - \"\"\" + \"\"\" Fooey \"\"\" |} " diff --git a/src/Fantomas.Tests/PatternMatchingTests.fs b/src/Fantomas.Tests/PatternMatchingTests.fs index ec92b0072d..ee6012abac 100644 --- a/src/Fantomas.Tests/PatternMatchingTests.fs +++ b/src/Fantomas.Tests/PatternMatchingTests.fs @@ -515,8 +515,7 @@ let update msg model = let res = match msg with | AMessage -> - { model with - AFieldWithAVeryVeryVeryLooooooongName = 10 } + { model with AFieldWithAVeryVeryVeryLooooooongName = 10 } .RecalculateTotal() | AnotherMessage -> model @@ -833,8 +832,8 @@ let private update onSubmit msg model = | UpdateCurrency c -> { model with Currency = c }, Cmd.none | UpdateLocation (lat, lng) -> { model with - Latitude = lat - Longitude = lng }, + Latitude = lat + Longitude = lng }, Cmd.none | UpdateIsDraft d -> { model with IsDraft = d }, Cmd.none | UpdateRemark r -> { model with Remark = r }, Cmd.none @@ -982,8 +981,8 @@ let draftToken = DraftToken.Create kind { token with - LeftColumn = token.LeftColumn - 1 - FullMatchedLength = token.FullMatchedLength + 1 } + LeftColumn = token.LeftColumn - 1 + FullMatchedLength = token.FullMatchedLength + 1 } | Some ({ Kind = SymbolKind.ActivePattern } as ap) when token.Tag = FSharpTokenTag.RPAREN -> DraftToken.Create SymbolKind.Ident ap.Token | _ -> diff --git a/src/Fantomas.Tests/RecordTests.fs b/src/Fantomas.Tests/RecordTests.fs index a21cbb52db..e12d4a7224 100644 --- a/src/Fantomas.Tests/RecordTests.fs +++ b/src/Fantomas.Tests/RecordTests.fs @@ -405,11 +405,11 @@ Fooey " let r = {| Foo = - a - && // && b - c + a + && // && b + c Bar = - \"\"\" + \"\"\" Fooey \"\"\" |} " @@ -429,8 +429,8 @@ let ``meaningful space should be preserved, 353`` () = """ to'.WithCommon (fun o' -> { dotnetOptions o' with - WorkingDirectory = Path.getFullName "RegressionTesting/issue29" - Verbosity = Some DotNet.Verbosity.Minimal }) + WorkingDirectory = Path.getFullName "RegressionTesting/issue29" + Verbosity = Some DotNet.Verbosity.Minimal }) .WithParameters """ @@ -533,22 +533,22 @@ type Database = .defaults( { Version = CurrentVersion Questions = - [| { Id = 0 - AuthorId = 1 - Title = \"What is the average wing speed of an unladen swallow?\" - Description = - \"\"\" + [| { Id = 0 + AuthorId = 1 + Title = \"What is the average wing speed of an unladen swallow?\" + Description = + \"\"\" Hello, yesterday I saw a flight of swallows and was wondering what their **average wing speed** is? If you know the answer please share it. \"\"\" - Answers = - [| { Id = 0 - CreatedAt = DateTime.Parse \"2017-09-14T19:57:33.103Z\" - AuthorId = 0 - Score = 2 - Content = - \"\"\" + Answers = + [| { Id = 0 + CreatedAt = DateTime.Parse \"2017-09-14T19:57:33.103Z\" + AuthorId = 0 + Score = 2 + Content = + \"\"\" > What do you mean, an African or European Swallow? > > Monty Python’s: The Holy Grail @@ -557,12 +557,12 @@ Ok I must admit, I use google to search the question and found a post explaining I thought you were asking it seriously, well done. x\"\"\" } - { Id = 1 - CreatedAt = DateTime.Parse \"2017-09-14T20:07:27.103Z\" - AuthorId = 2 - Score = 1 - Content = - \"\"\" + { Id = 1 + CreatedAt = DateTime.Parse \"2017-09-14T20:07:27.103Z\" + AuthorId = 2 + Score = 1 + Content = + \"\"\" Maxime, I believe you found [this blog post](http://www.saratoga.com/how-should-i-know/2013/07/what-is-the-average-air-speed-velocity-of-a-laden-swallow/). @@ -571,41 +571,76 @@ And so Robin, the conclusion of the post is: > In the end, it’s concluded that the airspeed velocity of a (European) unladen swallow is about 24 miles per hour or 11 meters per second. \"\"\" } |] - CreatedAt = DateTime.Parse \"2017-09-14T17:44:28.103Z\" } - { Id = 1 - AuthorId = 0 - Title = \"Why did you create Fable?\" - Description = - \"\"\" + CreatedAt = DateTime.Parse \"2017-09-14T17:44:28.103Z\" } + { Id = 1 + AuthorId = 0 + Title = \"Why did you create Fable?\" + Description = + \"\"\" Hello Alfonso, I wanted to know why you created Fable. Did you always plan to use F#? Or were you thinking in others languages? \"\"\" - Answers = [||] - CreatedAt = DateTime.Parse \"2017-09-12T09:27:28.103Z\" } |] + Answers = [||] + CreatedAt = DateTime.Parse \"2017-09-12T09:27:28.103Z\" } |] Users = - [| { Id = 0 - Firstname = \"Maxime\" - Surname = \"Mangel\" - Avatar = \"maxime_mangel.png\" } - { Id = 1 - Firstname = \"Robin\" - Surname = \"Munn\" - Avatar = \"robin_munn.png\" } - { Id = 2 - Firstname = \"Alfonso\" - Surname = \"Garciacaro\" - Avatar = \"alfonso_garciacaro.png\" } - { Id = 3 - Firstname = \"Guest\" - Surname = \"\" - Avatar = \"guest.png\" } |] } + [| { Id = 0 + Firstname = \"Maxime\" + Surname = \"Mangel\" + Avatar = \"maxime_mangel.png\" } + { Id = 1 + Firstname = \"Robin\" + Surname = \"Munn\" + Avatar = \"robin_munn.png\" } + { Id = 2 + Firstname = \"Alfonso\" + Surname = \"Garciacaro\" + Avatar = \"alfonso_garciacaro.png\" } + { Id = 3 + Firstname = \"Guest\" + Surname = \"\" + Avatar = \"guest.png\" } |] } ) .write () Logger.debug \"Database restored\" " +[] +let ``multiline string before closing brace`` () = + formatSourceString + false + " +let person = + let y = + let x = + { Story = \"\"\" + foo + bar +\"\"\" + } + () + () +" + config + |> prepend newline + |> should + equal + " +let person = + let y = + let x = + { Story = + \"\"\" + foo + bar +\"\"\" } + + () + + () +" + [] let ``issue 457`` () = formatSourceString @@ -627,8 +662,8 @@ let x = Foo("").Goo() let r = { s with - xxxxxxxxxxxxxxxxxxxxx = 1 - yyyyyyyyyyyyyyyyyyyyy = 2 } + xxxxxxxxxxxxxxxxxxxxx = 1 + yyyyyyyyyyyyyyyyyyyyy = 2 } """ [] @@ -682,18 +717,18 @@ let expect = let expect = Result.Ok { opts = - [ Opts.anyOf ( - [ (Optional, Opt.flagTrue [ "first"; "f" ]) - (Optional, Opt.value [ "second"; "s" ]) ] - ) - Opts.oneOf ( - Optional, - [ Opt.flag [ "third"; "f" ] - Opt.valueWith - "new value" - [ "fourth" - "ssssssssssssssssssssssssssssssssssssssssssssssssssss" ] ] - ) ] + [ Opts.anyOf ( + [ (Optional, Opt.flagTrue [ "first"; "f" ]) + (Optional, Opt.value [ "second"; "s" ]) ] + ) + Opts.oneOf ( + Optional, + [ Opt.flag [ "third"; "f" ] + Opt.valueWith + "new value" + [ "fourth" + "ssssssssssssssssssssssssssssssssssssssssssssssssssss" ] ] + ) ] args = [] commands = [] } """ @@ -761,11 +796,11 @@ open WebSharper.UI module Maintoc = let Page = { MyPage.Create() with - body = - [ Doc.Verbatim - \"\"\" + body = + [ Doc.Verbatim + \"\"\" This is a very long line in a multi-line string, so long in fact that it is longer than that page width to which I am trying to constrain everything, and so it goes bang. -\"\"\" ] } +\"\"\" ] } " [] @@ -1027,7 +1062,7 @@ let ``longer anonymous record with copy expression`` () = """ let foo = {| bar with - AMemberWithALongName = aValueWithAlsoALongName |} + AMemberWithALongName = aValueWithAlsoALongName |} """ [] @@ -1531,3 +1566,336 @@ match entities with Type = Elephant } |] -> () | _ -> () """ + +[] +let ``update record should indent from curly brace, 1876`` () = + formatSourceString + false + """ +let rainbow2 = + { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + config + |> prepend newline + |> should + equal + """ +let rainbow2 = + { rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + +[] +let ``update record should indent from curly brace, indent size 2`` () = + formatSourceString + false + """ +let rainbow2 = + { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + { config with IndentSize = 2 } + |> prepend newline + |> should + equal + """ +let rainbow2 = + { rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + +[] +let ``update record should indent from curly brace, indent size 3`` () = + formatSourceString + false + """ +let rainbow2 = + { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + { config with IndentSize = 3 } + |> prepend newline + |> should + equal + """ +let rainbow2 = + { rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + +[] +let ``record with comments above field`` () = + formatSourceString + false + """ +{ Foo = + // bar + someValue} +""" + config + |> prepend newline + |> should + equal + """ +{ Foo = + // bar + someValue } +""" + +[] +let ``record with comments above field, indent 2`` () = + formatSourceString + false + """ +{ Foo = + // bar + someValue} +""" + { config with IndentSize = 2 } + |> prepend newline + |> should + equal + """ +{ Foo = + // bar + someValue } +""" + +[] +let ``record with comments above field, indent 3`` () = + formatSourceString + false + """ +{ Foo = + // bar + someValue} +""" + { config with IndentSize = 3 } + |> prepend newline + |> should + equal + """ +{ Foo = + // bar + someValue } +""" + +[] +let ``anonymous record with multiline field`` () = + formatSourceString + false + """ +{| Foo = + // meh + someValue |} +""" + config + |> prepend newline + |> should + equal + """ +{| Foo = + // meh + someValue |} +""" + +[] +let ``anonymous record with multiline field, indent 2`` () = + formatSourceString + false + """ +{| Foo = + // meh + someValue |} +""" + { config with IndentSize = 2 } + |> prepend newline + |> should + equal + """ +{| Foo = + // meh + someValue |} +""" + +[] +let ``anonymous record with multiline field, indent 3`` () = + formatSourceString + false + """ +{| Foo = + // meh + someValue |} +""" + { config with IndentSize = 3 } + |> prepend newline + |> should + equal + """ +{| Foo = + // meh + someValue |} +""" + +[] +let ``anonymous record with multiline field, indent 5`` () = + formatSourceString + false + """ +{| Foo = + // meh + someValue |} +""" + { config with IndentSize = 5 } + |> prepend newline + |> should + equal + """ +{| Foo = + // meh + someValue |} +""" + +[] +let ``a foo`` () = + formatSourceString + false + """ +{| Foo = + someValue + // + a |} +""" + { config with IndentSize = 3 } + |> prepend newline + |> should + equal + """ +{| Foo = + someValue + // + a |} +""" + +[] +let ``long record field assigment`` () = + formatSourceString + false + """ +{ A = + // one indent starting from { + someFunctionCall + arg1 + arg2 + B = + // one indent starting from label B + someFunctionCall + arg1 + arg2 } +""" + config + |> prepend newline + |> should + equal + """ +{ A = + // one indent starting from { + someFunctionCall arg1 arg2 + B = + // one indent starting from label B + someFunctionCall arg1 arg2 } +""" + +[] +let ``anonymous update record, indent_size 3`` () = + formatSourceString + false + """ +{| f with Foo = + // meh + someValue |} +""" + { config with IndentSize = 3 } + |> prepend newline + |> should + equal + """ +{| f with + Foo = + // meh + someValue |} +""" + +[] +let ``restore correct indent after update record expression`` () = + formatSourceString + false + """ +let parse (checker: FSharpChecker) (parsingOptions: FSharpParsingOptions) { FileName = fileName; Source = source } = + let allDefineOptions, defineHashTokens = TokenParser.getDefines source + + allDefineOptions + |> List.map (fun conditionalCompilationDefines -> + async { + let parsingOptionsWithDefines = + { parsingOptions with + ConditionalCompilationDefines = conditionalCompilationDefines + SourceFiles = Array.map safeFileName parsingOptions.SourceFiles } + // Run the first phase (untyped parsing) of the compiler + let sourceText = + FSharp.Compiler.Text.SourceText.ofString source + + let! untypedRes = checker.ParseFile(fileName, sourceText, parsingOptionsWithDefines) + + if untypedRes.ParseHadErrors then + let errors = + untypedRes.Diagnostics + |> Array.filter (fun e -> e.Severity = FSharpDiagnosticSeverity.Error) + + if not <| Array.isEmpty errors then + raise + <| FormatException( + sprintf "Parsing failed with errors: %A\nAnd options: %A" errors parsingOptionsWithDefines + ) + + return (untypedRes.ParseTree, conditionalCompilationDefines, defineHashTokens) + }) + |> Async.Parallel +""" + config + |> prepend newline + |> should + equal + """ +let parse (checker: FSharpChecker) (parsingOptions: FSharpParsingOptions) { FileName = fileName; Source = source } = + let allDefineOptions, defineHashTokens = TokenParser.getDefines source + + allDefineOptions + |> List.map (fun conditionalCompilationDefines -> + async { + let parsingOptionsWithDefines = + { parsingOptions with + ConditionalCompilationDefines = conditionalCompilationDefines + SourceFiles = Array.map safeFileName parsingOptions.SourceFiles } + // Run the first phase (untyped parsing) of the compiler + let sourceText = + FSharp.Compiler.Text.SourceText.ofString source + + let! untypedRes = checker.ParseFile(fileName, sourceText, parsingOptionsWithDefines) + + if untypedRes.ParseHadErrors then + let errors = + untypedRes.Diagnostics + |> Array.filter (fun e -> e.Severity = FSharpDiagnosticSeverity.Error) + + if not <| Array.isEmpty errors then + raise + <| FormatException( + sprintf "Parsing failed with errors: %A\nAnd options: %A" errors parsingOptionsWithDefines + ) + + return (untypedRes.ParseTree, conditionalCompilationDefines, defineHashTokens) + }) + |> Async.Parallel +""" diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs index 07338913e2..09d2b3cb43 100644 --- a/src/Fantomas/CodePrinter.fs +++ b/src/Fantomas/CodePrinter.fs @@ -1373,7 +1373,7 @@ and genExpr astContext synExpr ctx = let longExpression = ifAlignBrackets (genMultilineAnonRecordAlignBrackets isStruct fields copyInfo astContext) - (genMultilineAnonRecord isStruct fields copyInfo astContext) + (genMultilineAnonRecord isStruct fields copyInfo synExpr.Range astContext) fun (ctx: Context) -> let size = getRecordSize ctx fields @@ -2613,7 +2613,7 @@ and genExpr astContext synExpr ctx = genExpr astContext optExpr +> genSynStaticOptimizationConstraint astContext constraints +> sepEq - +> sepSpaceOrNlnIfExpressionExceedsPageWidth (genExpr astContext e) + +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genExpr astContext e) | UnsupportedExpr r -> raise @@ -2982,56 +2982,76 @@ and genMultilineRecordInstance astContext (ctx: Context) = - let recordExpr = - let fieldsExpr = - col sepSemiNln xs (genRecordFieldName astContext) + let ifIndentLesserThan size lesser greater ctx = + if ctx.Config.IndentSize < size then + lesser ctx + else + greater ctx - match eo with - | Some e -> - genExpr astContext e - +> !- " with" - +> indent - +> sepNln - +> fieldsExpr - +> unindent - | None -> fieldsExpr + let expressionStartColumn = ctx.Column + + let fieldsExpr = + col sepSemiNln xs (genRecordFieldName astContext) let expr = - sepOpenS - +> (fun (ctx: Context) -> - { ctx with - RecordBraceStart = ctx.Column :: ctx.RecordBraceStart }) - +> atCurrentColumnIndent ( - leaveLeftBrace synExpr.Range - +> opt - (if xs.IsEmpty then sepNone else sepNln) - inheritOpt - (fun (typ, expr) -> - !- "inherit " - +> genType astContext false typ - +> addSpaceBeforeClassConstructor expr - +> genExpr astContext expr) - +> recordExpr - ) - +> (fun ctx -> - match ctx.RecordBraceStart with - | rbs :: rest -> - if ctx.Column < rbs then - let offset = - (if ctx.Config.SpaceAroundDelimiter then - 2 - else - 1) - + 1 - - let delta = Math.Max((rbs - ctx.Column) - offset, 0) - (!- System.String.Empty.PadRight(delta)) { ctx with RecordBraceStart = rest } - else - sepNone { ctx with RecordBraceStart = rest } - | [] -> sepNone ctx) - +> sepNlnWhenWriteBeforeNewlineNotEmpty sepNone - +> enterNodeTokenByName synExpr.Range RBRACE - +> ifElseCtx lastWriteEventIsNewline sepCloseSFixed sepCloseS + match inheritOpt with + | Some (t, e) -> + tokN synExpr.Range LBRACE sepOpenS + +> atCurrentColumn ( + !- "inherit " + +> genType astContext false t + +> addSpaceBeforeClassConstructor e + +> genExpr astContext e + +> onlyIf (List.isNotEmpty xs) sepNln + +> fieldsExpr + +> tokN synExpr.Range RBRACE sepCloseS + ) + | None -> + match eo with + | None -> + fun (ctx: Context) -> + // position after `{ ` or `{` + let targetColumn = + ctx.Column + + (if ctx.Config.SpaceAroundDelimiter then + 2 + else + 1) + + atCurrentColumn + (tokN synExpr.Range LBRACE sepOpenS + +> sepNlnWhenWriteBeforeNewlineNotEmpty sepNone // comment after curly brace + +> col + sepSemiNln + xs + (fun e -> + // Add spaces to ensure the record field (incl trivia) starts at the right column. + addFixedSpaces targetColumn + // Lock the start of the record field, however keep potential indentations in relation to the opening curly brace + +> atCurrentColumn (genRecordFieldName astContext e)) + +> tokN + synExpr.Range + RBRACE + (sepNlnWhenWriteBeforeNewlineNotEmpty sepNone // comment after last record field + +> (fun ctx -> + // Edge case scenario to make sure that the closing brace is not before the opening one + // See unit test "multiline string before closing brace" + let delta = expressionStartColumn - ctx.Column + + if delta > 0 then + ((rep delta (!- " ")) +> sepCloseSFixed) ctx + else + ifElseCtx lastWriteEventIsNewline sepCloseSFixed sepCloseS ctx))) + ctx + | Some e -> + tokN synExpr.Range LBRACE sepOpenS + +> genExpr astContext e + +> !- " with" + +> ifIndentLesserThan + 3 + (sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth fieldsExpr) + (sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth fieldsExpr) + +> tokN synExpr.Range RBRACE sepCloseS expr ctx @@ -3088,25 +3108,52 @@ and genMultilineRecordInstanceAlignBrackets +> sepCloseSFixed) |> atCurrentColumnIndent -and genMultilineAnonRecord (isStruct: bool) fields copyInfo astContext = +and genMultilineAnonRecord (isStruct: bool) fields copyInfo (range: Range) (astContext: ASTContext) = let recordExpr = - let fieldsExpr = - col sepSemiNln fields (genAnonRecordFieldName astContext) - match copyInfo with | Some e -> - genExpr astContext e - +> (!- " with" - +> indent - +> sepNln - +> fieldsExpr - +> unindent) - | None -> fieldsExpr + (tokN range LBRACK_BAR sepOpenAnonRecd) + +> atCurrentColumn ( + genExpr astContext e + +> (!- " with" + +> indent + +> sepNln + +> col sepSemiNln fields (genAnonRecordFieldName astContext) + +> unindent) + ) + +> (tokN range BAR_RBRACK sepCloseAnonRecd) + | None -> + fun ctx -> + // position after `{| ` or `{|` + let targetColumn = + ctx.Column + + (if ctx.Config.SpaceAroundDelimiter then + 3 + else + 2) + + atCurrentColumn + ((tokN range LBRACK_BAR sepOpenAnonRecd) + +> col + sepSemiNln + fields + (fun (AnonRecordFieldName (s, e)) -> + let expr = + if ctx.Config.IndentSize < 3 then + sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth (genExpr astContext e) + else + sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genExpr astContext e) + + // Add enough spaces to start at the right column but indent from the opening curly brace. + // Use a double indent when using a small indent size to avoid offset warnings. + addFixedSpaces targetColumn + +> !-s + +> sepEq + +> expr) + +> (tokN range BAR_RBRACK sepCloseAnonRecd)) + ctx - onlyIf isStruct !- "struct " - +> sepOpenAnonRecd - +> atCurrentColumnIndent recordExpr - +> sepCloseAnonRecd + onlyIf isStruct !- "struct " +> recordExpr and genMultilineAnonRecordAlignBrackets (isStruct: bool) fields copyInfo astContext = let fieldsExpr = diff --git a/src/Fantomas/Context.fs b/src/Fantomas/Context.fs index 93f8a1b81b..99bcf333c0 100644 --- a/src/Fantomas/Context.fs +++ b/src/Fantomas/Context.fs @@ -182,7 +182,6 @@ type internal Context = Content: string TriviaMainNodes: Map TriviaTokenNodes: Map - RecordBraceStart: int list FileName: string } /// Initialize with a string writer and use space as delimiter @@ -195,7 +194,6 @@ type internal Context = Content = "" TriviaMainNodes = Map.empty TriviaTokenNodes = Map.empty - RecordBraceStart = [] FileName = String.Empty } static member Create @@ -676,6 +674,12 @@ let internal sepSpace (ctx: Context) = | None -> ctx | _ -> (!- " ") ctx +// add actual spaces until the target column is reached, regardless of previous content +// use with care +let internal addFixedSpaces (targetColumn: int) (ctx: Context) : Context = + let delta = targetColumn - ctx.Column + onlyIf (delta > 0) (rep delta (!- " ")) ctx + let internal sepNln = !+ "" // Use a different WriteLine event to indicate that the newline was introduces due to trivia @@ -996,20 +1000,20 @@ let internal sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth expr (ctx: Conte expr ctx -let internal sepSpaceWhenOrIndentAndNlnIfExpressionExceedsPageWidth (addSpace: Context -> bool) expr (ctx: Context) = +let internal sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth expr (ctx: Context) = expressionExceedsPageWidth - (ifElseCtx addSpace sepSpace sepNone) + sepSpace sepNone // before and after for short expressions - (indent +> sepNln) - unindent // before and after for long expressions + (indent +> indent +> sepNln) + (unindent +> unindent) // before and after for long expressions expr ctx -let internal sepSpaceOrNlnIfExpressionExceedsPageWidth expr (ctx: Context) = +let internal sepSpaceWhenOrIndentAndNlnIfExpressionExceedsPageWidth (addSpace: Context -> bool) expr (ctx: Context) = expressionExceedsPageWidth - sepSpace + (ifElseCtx addSpace sepSpace sepNone) sepNone // before and after for short expressions - sepNln + (indent +> sepNln) unindent // before and after for long expressions expr ctx @@ -1141,13 +1145,13 @@ let internal ifAlignBrackets f g = ifElseCtx (fun ctx -> ctx.Config.MultilineBlockBracketsOnSameColumn) f g let internal printTriviaContent (c: TriviaContent) (ctx: Context) = - let currentLastLine = lastWriteEventOnLastLine ctx + let currentLastLine = ctx.WriterModel.Lines |> List.tryHead // Some items like #if or Newline should be printed on a newline // It is hard to always get this right in CodePrinter, so we detect it based on the current code. let addNewline = currentLastLine - |> Option.map (fun line -> line.Length > 0) + |> Option.map (fun line -> line.Trim().Length > 0) |> Option.defaultValue false let addSpace =