diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a843352c4b..a192a52a65 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,13 +9,13 @@ ] }, "fantomas": { - "version": "5.2.0", + "version": "5.2.1", "commands": [ "fantomas" ] }, "fsdocs-tool": { - "version": "17.0.0", + "version": "17.2.2", "commands": [ "fsdocs" ] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 992d0d28a3..cfa632ae86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ env: jobs: build: - + continue-on-error: true strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] diff --git a/CHANGELOG.md b/CHANGELOG.md index 659f5d86b6..6b78b2047e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## [6.0.0-alpha-007] - 2023-03-27 + +### Changed + +* `CodeFormatter.FormatASTAsync` returns a string. [#2799](https://github.com/fsprojects/fantomas/pull/2799) +* Cursor with defines. [#2774](https://github.com/fsprojects/fantomas/pull/2774) + +### Removed + +* Strict mode. [#2798](https://github.com/fsprojects/fantomas/pull/2798) + +## [6.0.0-alpha-006] - 2023-03-17 + +### Fixed +* Record member declarations can break with Stroustrup enabled. [#2787](https://github.com/fsprojects/fantomas/issues/2787) + +### Changed +* Add `fsharp_experimental_elmish` setting. [#2795](https://github.com/fsprojects/fantomas/pull/2795) + +## [6.0.0-alpha-005] - 2023-02-24 + +### Changed +* Fix handling of AppExpr with a single stroustrup record. [#2747](https://github.com/fsprojects/fantomas/pull/2747) +* Inconsistent styling when using Stroustrup with or without member attach to record creation. [#2652](https://github.com/fsprojects/fantomas/issues/2652) + +## [6.0.0-alpha-004] - 2023-02-22 + +### Changed +* Always process folder recursive. [#2768](https://github.com/fsprojects/fantomas/issues/2768) +* Revisit --profile flag. [#2751](https://github.com/fsprojects/fantomas/issues/2751) + +### Fixed +* Don't hook up SerilogTraceListener in FantomasDaemon. [#2777](https://github.com/fsprojects/fantomas/pull/2777) + +## [6.0.0-alpha-003] - 2023-02-04 + +### Changed +* Splitting ExperimentalStroustrupStyle to separate settings. [#2276](https://github.com/fsprojects/fantomas/issues/2276) +* Remove F# option from public API. [#2759](https://github.com/fsprojects/fantomas/pull/2759) +* Naive parallel formatting implementation. [#2717](https://github.com/fsprojects/fantomas/pull/2717) +* Add separate CodeFormatter.FormatDocumentAsync overloads with cursor and config. [#2763](https://github.com/fsprojects/fantomas/pull/2763) + +## [6.0.0-alpha-002] - 2023-02-01 + +### Changed +* Update output for copy-and-update expression for Stroustrup. [#2748](https://github.com/fsprojects/fantomas/pull/2748) +* Drop Experimental prefix from ExperimentalStroustrup. [#2755](https://github.com/fsprojects/fantomas/pull/2755) +* Expose initial Oak API. [#2758](https://github.com/fsprojects/fantomas/pull/2758) + +## [6.0.0-alpha-001] - 2023-01-24 + +### Changed +* Add `--verbosity` flag. [#2693](https://github.com/fsprojects/fantomas/pull/2693) +* Sunset MultilineBlockBracketsOnSameColumn & ExperimentalStroustrupStyle. [#2710](https://github.com/fsprojects/fantomas/issues/2710) +* Move FormatConfig into Fantomas.Core namespace. [#2736](https://github.com/fsprojects/fantomas/pull/2736) +* Initial cursor API. [#2739](https://github.com/fsprojects/fantomas/pull/2739) + ## [5.2.4] - 2023-03-17 ### Fixed diff --git a/build.fsx b/build.fsx index 31113411f9..4726a68d60 100644 --- a/build.fsx +++ b/build.fsx @@ -1,4 +1,4 @@ -#r "nuget: Fun.Build, 0.1.8" +#r "nuget: Fun.Build, 0.3.1" #r "nuget: CliWrap, 3.5.0" #r "nuget: FSharp.Data, 5.0.2" @@ -57,14 +57,14 @@ pipeline "Build" { run ( cleanFolders [| "bin" - "src/Fantomas.FCS/bin" - "src/Fantomas.FCS/obj" - "src/Fantomas.Core/bin" - "src/Fantomas.Core/obj" - "src/Fantomas/bin" - "src/Fantomas/obj" - "src/Fantomas.Client/bin" - "src/Fantomas.Client/obj" |] + "src/Fantomas.FCS/bin/Release" + "src/Fantomas.FCS/obj/Release" + "src/Fantomas.Core/bin/Release" + "src/Fantomas.Core/obj/Release" + "src/Fantomas/bin/Release" + "src/Fantomas/obj/Release" + "src/Fantomas.Client/bin/Release" + "src/Fantomas.Client/obj/Release" |] ) } stage "CheckFormat" { run "dotnet fantomas src docs build.fsx --recurse --check" } diff --git a/docs/content/webcomponents.js b/docs/content/webcomponents.js index b6dd7774c5..21f18b9c93 100644 --- a/docs/content/webcomponents.js +++ b/docs/content/webcomponents.js @@ -1,7 +1,7 @@ import {html} from 'https://cdn.skypack.dev/lit'; import {component} from 'https://cdn.skypack.dev/haunted'; -function FantomasSettingIcon({type}) { +function FantomasSettingIconCore(type) { let settingType switch (type) { case 'green': @@ -20,9 +20,7 @@ function FantomasSettingIcon({type}) { break; case 'red': settingType = { - icon: "bi-x-circle-fill", - color: "red-recommendation", - tooltip: "You shouldn't use this setting." + icon: "bi-x-circle-fill", color: "red-recommendation", tooltip: "You shouldn't use this setting." } break; case 'gr': @@ -35,7 +33,7 @@ function FantomasSettingIcon({type}) { data-bs-title="${tooltip}" src="${root}/images/gresearch.svg" alt="G-Research logo"/>`; default: - throw `The "type" can only be "green", "orange", "red" or "gr". Found "${type}"`; + throw "The \"type\" can only be \"green\", \"orange\", \"red\" or \"gr\""; } return html` - ${green && FantomasSettingIcon({type: "green"})} - ${orange && FantomasSettingIcon({type: "orange"})} - ${red && FantomasSettingIcon({type: "red"})} - ${gr && FantomasSettingIcon({type: "gr"})} + ${green && FantomasSettingIconCore('green')} + ${orange && FantomasSettingIconCore('orange')} + ${red && FantomasSettingIconCore('red')} + ${gr && FantomasSettingIconCore('gr')}

${name}

` } +function FantomasSettingIcon({green, orange, red, gr}) { + return html` + ${green && FantomasSettingIconCore('green')} + ${orange && FantomasSettingIconCore('orange')} + ${red && FantomasSettingIconCore('red')} + ${gr && FantomasSettingIconCore('gr')} + ` +} + customElements.define('fantomas-setting-icon', component(FantomasSettingIcon, { - useShadowDOM: false, observedAttributes: ['type'] + useShadowDOM: false, observedAttributes: ['green', 'orange', 'red', 'gr'] })); + customElements.define('fantomas-setting', component(FantomasSetting, { useShadowDOM: false, observedAttributes: ['name', 'green', 'orange', 'red', 'gr'] })); diff --git a/docs/docs/contributors/Solution Structure.md b/docs/docs/contributors/Solution Structure.md index 5ffee7da8f..eec2ccd51d 100644 --- a/docs/docs/contributors/Solution Structure.md +++ b/docs/docs/contributors/Solution Structure.md @@ -15,7 +15,7 @@ graph TD B --> D[Fantomas.Benchmarks] B --> E[Fantomas.Core.Tests] C --> F[Fantomas.Tests] - G[Fantomas.Client] + G[Fantomas.Client] --> H[Fantomas.Client.Tests] ## Fantomas.FCS @@ -57,4 +57,8 @@ A suite of unit tests that target the core formatting functionalities of `Fantom A suite of end-to-end tests that run the actual `fantomas` command line application. +## Fantomas.Client.Tests + +A suite of end-to-end tests that will verify the `Fantomas.Client` code against released versions of `fantomas`. + diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index a4af68e244..7b90fe74fc 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -15,6 +15,12 @@ Your IDE should respect your settings, however the implementation of that is edi UI might be available depending on the IDE. *) +#r "../../../src/Fantomas/bin/Release/net6.0/Fantomas.FCS.dll" +#r "../../../src/Fantomas/bin/Release/net6.0/Fantomas.Core.dll" + +printf $"version: {Fantomas.Core.CodeFormatter.GetVersion()}" +(*** include-output ***) + (** ## Usage Inside .editorconfig you can specify the file extension and code location to be use per config: @@ -28,7 +34,7 @@ fsharp_bar_before_discriminated_union_declaration = true #\ Apply specific settings for a targeted subfolder [src/Elmish/View.fs] -fsharp_multiline_bracket_style = experimental_stroustrup +fsharp_multiline_bracket_style = stroustrup ``` *) @@ -39,23 +45,23 @@ You can quickly try your settings via the *) -#r "nuget: Fantomas.Core, 5.*" - -open Fantomas.Core.FormatConfig open Fantomas.Core let formatCode input configIndent = - CodeFormatter.FormatDocumentAsync(false, input, configIndent) + async { + let! result = CodeFormatter.FormatDocumentAsync(false, input, configIndent) + printf $"%s{result.Code}" + } |> Async.RunSynchronously (** ## Settings recommendations Fantomas ships with a series of settings that you can use freely depending on your case. However, there are settings that we do not recommend and generally should not be used. -

Safe to change: Settings that aren't attached to any guidelines. Depending on your team or your own preferences, feel free to change these as it's been agreed on the codebase, however, you can always use it's defaults.

-

Use with caution: Settings where it is not recommended to change the default value. They might lead to incomplete results.

-

Do not use: Settings that don't follow any guidelines.

-

G-Research: G-Research styling guide. If you use one of these, for consistency reasons you should use all of them.

+

Safe to change: Settings that aren't attached to any guidelines. Depending on your team or your own preferences, feel free to change these as it's been agreed on the codebase, however, you can always use it's defaults.

+

Use with caution: Settings where it is not recommended to change the default value. They might lead to incomplete results.

+

Do not use: Settings that don't follow any guidelines.

+

G-Research: G-Research styling guide. If you use one of these, for consistency reasons you should use all of them.

*) (** @@ -87,7 +93,7 @@ formatCode """ { FormatConfig.Default with IndentSize = 2 } -(*** include-it ***) +(*** include-output ***) (** @@ -105,7 +111,7 @@ formatCode """ { FormatConfig.Default with MaxLineLength = 60 } -(*** include-it ***) +(*** include-output ***) (** @@ -130,7 +136,7 @@ formatCode """ { FormatConfig.Default with InsertFinalNewline = false } -(*** include-it ***) +(*** include-output ***) (** @@ -148,7 +154,7 @@ formatCode """ { FormatConfig.Default with SpaceBeforeParameter = false } -(*** include-it ***) +(*** include-output ***) (** @@ -169,7 +175,7 @@ match x with """ { FormatConfig.Default with SpaceBeforeLowercaseInvocation = false } -(*** include-it ***) +(*** include-output ***) (** @@ -190,7 +196,7 @@ match x with """ { FormatConfig.Default with SpaceBeforeUppercaseInvocation = true } -(*** include-it ***) +(*** include-output ***) (** @@ -209,7 +215,7 @@ formatCode { FormatConfig.Default with SpaceBeforeClassConstructor = true } -(*** include-it ***) +(*** include-output ***) (** @@ -229,7 +235,7 @@ formatCode """ { FormatConfig.Default with SpaceBeforeMember = true } -(*** include-it ***) +(*** include-output ***) (** @@ -247,7 +253,7 @@ formatCode """ { FormatConfig.Default with SpaceBeforeColon = true } -(*** include-it ***) +(*** include-output ***) (** @@ -264,7 +270,7 @@ formatCode """ { FormatConfig.Default with SpaceAfterComma = false } -(*** include-it ***) +(*** include-output ***) (** @@ -282,7 +288,7 @@ formatCode """ { FormatConfig.Default with SpaceBeforeSemicolon = true } -(*** include-it ***) +(*** include-output ***) (** @@ -300,7 +306,7 @@ formatCode """ { FormatConfig.Default with SpaceAfterSemicolon = false } -(*** include-it ***) +(*** include-output ***) (** @@ -317,7 +323,7 @@ formatCode """ { FormatConfig.Default with SpaceAroundDelimiter = false } -(*** include-it ***) +(*** include-output ***) (** ## Maximum width constraints @@ -341,7 +347,7 @@ formatCode """ { FormatConfig.Default with MaxIfThenShortWidth = 15 } -(*** include-it ***) +(*** include-output ***) (** @@ -358,7 +364,7 @@ formatCode """ { FormatConfig.Default with MaxIfThenElseShortWidth = 10 } -(*** include-it ***) +(*** include-output ***) (** @@ -374,7 +380,7 @@ formatCode """ { FormatConfig.Default with MaxInfixOperatorExpression = 20 } -(*** include-it ***) +(*** include-output ***) (** @@ -393,7 +399,7 @@ formatCode """ { FormatConfig.Default with MaxRecordWidth = 20 } -(*** include-it ***) +(*** include-output ***) (** @@ -425,7 +431,7 @@ formatCode { FormatConfig.Default with MaxRecordNumberOfItems = 2 RecordMultilineFormatter = MultilineFormatterType.NumberOfItems } -(*** include-it ***) +(*** include-output ***) (** @@ -453,7 +459,7 @@ formatCode """ { FormatConfig.Default with RecordMultilineFormatter = MultilineFormatterType.NumberOfItems } -(*** include-it ***) +(*** include-output ***) (** @@ -471,7 +477,7 @@ formatCode """ { FormatConfig.Default with MaxArrayOrListWidth = 20 } -(*** include-it ***) +(*** include-output ***) (** @@ -491,7 +497,7 @@ formatCode { FormatConfig.Default with MaxArrayOrListNumberOfItems = 2 ArrayOrListMultilineFormatter = MultilineFormatterType.NumberOfItems } -(*** include-it ***) +(*** include-output ***) (** @@ -511,7 +517,7 @@ formatCode """ { FormatConfig.Default with ArrayOrListMultilineFormatter = MultilineFormatterType.NumberOfItems } -(*** include-it ***) +(*** include-output ***) (** @@ -530,7 +536,7 @@ formatCode """ { FormatConfig.Default with MaxValueBindingWidth = 10 } -(*** include-it ***) +(*** include-output ***) (** @@ -549,7 +555,7 @@ formatCode """ { FormatConfig.Default with MaxFunctionBindingWidth = 10 } -(*** include-it ***) +(*** include-output ***) (** @@ -569,63 +575,91 @@ formatCode """ { FormatConfig.Default with MaxDotGetExpressionWidth = 100 } -(*** include-it ***) +(*** include-output ***) (** - - -How to format bracketted expressions (e.g. records, arrays, lists, etc.) that span multiple lines. + -_This setting replaces the deprecated settings `fsharp_multiline_block_brackets_on_same_column` and `fsharp_experimental_stroustrup_style`._ +`Cramped` The default way in F# to format brackets. +`Aligned` Alternative way of formatting records, arrays and lists. This will align the braces at the same column level. +`Stroustrup` Allow for easier reordering of members and keeping the code succinct. -Possible values: - -* `cramped` -* `aligned` -* `experimental_stroustrup` - -Default = `cramped`. +Default = Cramped. *) -(** -**Cramped** - The default way in F# to format brackets. -*) formatCode """ - let band = { Vocals = "John"; Bass = "Paul"; Guitar = "George"; Drums = "Ringo" } - let songs = [ "Come Together"; "Hey Jude"; "Yesterday"; "Yellow Submarine"; "Here Comes the Sun" ] + let myRecord = + { Level = 1 + Progress = "foo" + Bar = "bar" + Street = "Bakerstreet" + Number = 42 } + + type Range = + { From: float + To: float + FileName: string } + + let a = + [| (1, 2, 3) + (4, 5, 6) + (7, 8, 9) + (10, 11, 12) + (13, 14, 15) + (16, 17,18) + (19, 20, 21) |] """ { FormatConfig.Default with - MultilineBracketStyle = Cramped } -(*** include-it ***) - -(** -**Aligned** - Alternative way of formatting brackets. This will align the braces at the same column level. -*) + MultilineBracketStyle = Aligned } +(*** include-output ***) formatCode """ - let band = { Vocals = "John"; Bass = "Paul"; Guitar = "George"; Drums = "Ringo" } - let songs = [ "Come Together"; "Hey Jude"; "Yesterday"; "Yellow Submarine"; "Here Comes the Sun" ] + let myRecord = + { Level = 1 + Progress = "foo" + Bar = "bar" + Street = "Bakerstreet" + Number = 42 } + + type Range = + { From: float + To: float + FileName: string } + + let a = + [| (1, 2, 3) + (4, 5, 6) + (7, 8, 9) + (10, 11, 12) + (13, 14, 15) + (16, 17,18) + (19, 20, 21) |] """ { FormatConfig.Default with - MultilineBracketStyle = Aligned } -(*** include-it ***) + MultilineBracketStyle = Stroustrup } +(*** include-output ***) (** -**ExperimentalStroustrup** - Experimental setting. Places the opening brace on the same line as the binding, and the closing brace on its own line. + + +Insert a newline before a computation expression that spans multiple lines -_Please contribute to [fsprojects/fantomas#1408](https://github.com/fsprojects/fantomas/issues/1408) and engage in [fsharp/fslang-design#706](https://github.com/fsharp/fslang-design/issues/706)._ +Default = true *) formatCode """ - let band = { Vocals = "John"; Bass = "Paul"; Guitar = "George"; Drums = "Ringo" } - let songs = [ "Come Together"; "Hey Jude"; "Yesterday"; "Yellow Submarine"; "Here Comes the Sun" ] + let something = + task { + let! thing = otherThing () + return 5 + } """ { FormatConfig.Default with - MultilineBracketStyle = ExperimentalStroustrup } -(*** include-it ***) + NewlineBeforeMultilineComputationExpression = false } +(*** include-output ***) (** ## G-Research style @@ -649,7 +683,7 @@ type Range = """ { FormatConfig.Default with NewlineBetweenTypeDefinitionAndMembers = true } -(*** include-it ***) +(*** include-output ***) (** @@ -671,7 +705,7 @@ let run """ { FormatConfig.Default with AlignFunctionSignatureToIndentation = true } -(*** include-it ***) +(*** include-output ***) (** @@ -704,7 +738,7 @@ type D() = """ { FormatConfig.Default with AlternativeLongMemberDefinitions = true } -(*** include-it ***) +(*** include-output ***) (** @@ -732,7 +766,7 @@ let printListWithOffset a list1 = """ { FormatConfig.Default with MultiLineLambdaClosingNewline = true } -(*** include-it ***) +(*** include-output ***) (** @@ -762,7 +796,7 @@ let main argv = """ { FormatConfig.Default with ExperimentalKeepIndentInBranch = true } -(*** include-it ***) +(*** include-output ***) (** @@ -780,7 +814,7 @@ formatCode { FormatConfig.Default with BarBeforeDiscriminatedUnionDeclaration = true } -(*** include-it ***) +(*** include-output ***) (** ## Other @@ -814,7 +848,7 @@ formatCode """ { FormatConfig.Default with BlankLinesAroundNestedMultilineExpressions = false } -(*** include-it ***) +(*** include-output ***) (** @@ -832,34 +866,48 @@ formatCode """ { FormatConfig.Default with KeepMaxNumberOfBlankLines = 1 } -(*** include-it ***) +(*** include-output ***) (** - + -If being set, pretty printing is only done via ASTs. Compiler directives, inline comments and block comments will be ignored. -There are numerous situations when the information in the AST alone cannot restore the original code. +Applies the Stroustrup style to the final (two) array or list argument(s) in a function application. +Note that this behaviour is also active when `fsharp_multiline_bracket_style = stroustrup`. -**Please do not use this setting for formatting hand written code!** - -Valid use-case of this settings is code generation in projects like [FsAst](https://github.com/ionide/FsAst) and [Myriad](https://github.com/MoiraeSoftware/myriad). - -Default = false. +Default = false *) formatCode """ - // some great comment - let add a b = - #if INTERACTIVE - 42 - #else - a + b - #endif +let dualList = + div + [] + [ + h1 [] [ str "Some title" ] + ul + [] + [ + for p in model.Points do + li [] [ str $"%i{p.X}, %i{p.Y}" ] + ] + hr [] + ] + +let singleList = + Html.div + [ + Html.h1 [ str "Some title" ] + Html.ul + [ + for p in model.Points do + Html.li [ str $"%i{p.X}, %i{p.Y}" ] + ] + ] """ { FormatConfig.Default with - StrictMode = true } -(*** include-it ***) + ExperimentalElmish = true } +(*** include-output ***) + (** diff --git a/docs/docs/end-users/FAQ.md b/docs/docs/end-users/FAQ.md index 782c02f4ee..a6dae9c506 100644 --- a/docs/docs/end-users/FAQ.md +++ b/docs/docs/end-users/FAQ.md @@ -44,4 +44,4 @@ without the compiler nagging you about the missing space between the callee (`Pr Since F# 6.0, Fantomas interprets the list as an index expression and formats it accordingly. In such a case, just add a space between the callee and the list and you should be good to go. - + diff --git a/docs/docs/end-users/GeneratingCode.fsx b/docs/docs/end-users/GeneratingCode.fsx index 1bd7e4a92e..4cec53a859 100644 --- a/docs/docs/end-users/GeneratingCode.fsx +++ b/docs/docs/end-users/GeneratingCode.fsx @@ -32,83 +32,63 @@ In simple scenarios this can work out, but in the long run it doesn't scale well To illustrate the API, lets generate a simple value binding: `let a = 0`. *) -#r "nuget: Fantomas.Core, 5.*" // Note that this will also load Fantomas.FCS, which contains the syntax tree types. +#r "../../../src/Fantomas/bin/Release/net6.0/Fantomas.FCS.dll" +#r "../../../src/Fantomas/bin/Release/net6.0/Fantomas.Core.dll" // In production use #r "nuget: Fantomas.Core, 6.0-alpha-*" open FSharp.Compiler.Text -open FSharp.Compiler.Xml -open FSharp.Compiler.Syntax -open FSharp.Compiler.SyntaxTrivia +open Fantomas.Core.SyntaxOak let implementationSyntaxTree = - ParsedInput.ImplFile( - ParsedImplFileInput( - "filename.fsx", - true, - QualifiedNameOfFile(Ident("", Range.Zero)), - [], - [], - [ SynModuleOrNamespace( - [], - false, - SynModuleOrNamespaceKind.AnonModule, - [ SynModuleDecl.Let( - false, - [ SynBinding( - None, - SynBindingKind.Normal, - false, - false, - [], - PreXmlDoc.Empty, - SynValData(None, SynValInfo([], SynArgInfo([], false, None)), None), - SynPat.Named(SynIdent(Ident("a", Range.Zero), None), false, None, Range.Zero), - None, - SynExpr.Const(SynConst.Int32(0), Range.Zero), - Range.Zero, - DebugPointAtBinding.Yes Range.Zero, - { EqualsRange = Some Range.Zero - InlineKeyword = None - LeadingKeyword = SynLeadingKeyword.Let Range.Zero } - ) ], - Range.Zero - ) ], - PreXmlDoc.Empty, - [], - None, - Range.Zero, - { LeadingKeyword = SynModuleOrNamespaceLeadingKeyword.None } - ) ], - (false, false), - { ConditionalDirectives = [] - CodeComments = [] }, - Set.empty - ) + Oak( + [], + [ ModuleOrNamespaceNode( + None, + [ BindingNode( + None, + None, + MultipleTextsNode([ SingleTextNode("let", Range.Zero) ], Range.Zero), + false, + None, + None, + Choice1Of2(IdentListNode([ IdentifierOrDot.Ident(SingleTextNode("a", Range.Zero)) ], Range.Zero)), + None, + [], + None, + SingleTextNode("=", Range.Zero), + Expr.Constant(Constant.FromText(SingleTextNode("0", Range.Zero))), + Range.Zero + ) + |> ModuleDecl.TopLevelBinding ], + Range.Zero + ) ], + Range.Zero ) open Fantomas.Core -CodeFormatter.FormatASTAsync(implementationSyntaxTree) |> Async.RunSynchronously -(*** include-it ***) +CodeFormatter.FormatOakAsync(implementationSyntaxTree) +|> Async.RunSynchronously +|> printfn "%s" +(*** include-output ***) (** Constructing the entire syntax tree can be a bit overwhelming at first. There is a lot of information to provide and a lot to unpack if you have never seen any of this before. Let's deconstruct a couple of things: -- Every file has one or more [SynModuleOrNamespace](../../reference/fsharp-compiler-syntax-synmoduleornamespace.html). In this case the module was anonymous and thus invisible. -- Every `SynModuleOrNamespace` has top level [SynModuleDecl](../../https://fsprojects.github.io/fantomas/reference/fsharp-compiler-syntax-synmoduledecl.html). -- [SynModuleDecl.Let](../../https://fsprojects.github.io/fantomas/reference/fsharp-compiler-syntax-synmoduledecl.html#Let) takes one or more [SynBinding](../../reference/fsharp-compiler-syntax-synbinding.html). +- Every file has one or more [ModuleOrNamespaceNode](../../reference/fantomas-core-syntaxoak-moduleornamespacenode.html). In this case the module was anonymous and thus invisible. +- Every `ModuleOrNamespaceNode` has top level [ModuleDecl](../../reference/fantomas-core-syntaxoak-moduledecl.html). +- [ModuleDecl.TopLevelBinding](../../https://fsprojects.github.io/fantomas/reference/fantomas-core-syntaxoak-moduledecl.html#TopLevelBinding) takes a [BindingNode ](../../reference/fantomas-core-syntaxoak-bindingnode.html). - You would have multiple bindings in case of a recursive function. -- The `headPat` of binding contains the name and the parameters. -- The `expr` ([SynExpr](../../reference/fsharp-compiler-syntax-synexpr.html)) represents the F# syntax expression. +- The `functionName ` of binding contains the name or is a pattern. +- The `expr` ([Expr](../../reference/fantomas-core-syntaxoak-expr.html)) represents the F# syntax expression. - Because there is no actual source code, all ranges will be `Range.Zero`. -The more you interact with AST, the easier you pick up which node represents what. +The more you interact with AST/Oak, the easier you pick up which node represents what. ### Fantomas.FCS -When looking at the example, we notice that we've opened a couple of `FSharp.Compiler.*` namespaces. +When looking at the example, we notice that we've opened `FSharp.Compiler.Text`. Don't be fooled by this, `Fantomas.Core` and `Fantomas.FCS` **do not reference [FSharp.Compiler.Service](https://www.nuget.org/packages/FSharp.Compiler.Service)**! Instead, `Fantomas.FCS` is a custom version of the F# compiler (built from source) that only exposes the F# parser and the syntax tree. @@ -121,14 +101,17 @@ Example usage: *) -#r "nuget: Fantomas.FCS" - -open FSharp.Compiler.Text open Fantomas.FCS Parse.parseFile false (SourceText.ofString "let a = 1") [] (*** include-it ***) +(** +You can format untyped AST created from `Fantomas.FCS` using the `CodeFormatter` API. +However, we recommend to use the new `Oak` model (as in the example) instead. +The `Oak` model is easier to reason with as it structures certain concepts differently than the untyped AST. +*) + (** ## Tips and tricks @@ -137,77 +120,39 @@ Parse.parseFile false (SourceText.ofString "let a = 1") [] The syntax tree can have an overwhelming type hierarchy. We wholeheartedly recommend to use our **[online tool](https://fsprojects.github.io/fantomas-tools/#/ast)** when working with AST. -![F# AST Viewer](../../images/ast-viewer.png) +![F# AST Viewer](../../images/oak-viewer.png) -This shows you what AST nodes the parser created for a given input text. +This shows you what Oak nodes the parser created for a given input text. From there on you can use our search bar to find the corresponding documentation: ![Search bar](../../images/searchbar-ast.png) ### Match the AST the parser would produce -Fantomas will very selectively use information from the AST. -Please make sure you construct the same AST as the parser would. +Fantomas will very selectively use information from the AST to construct the Oak. +Please make sure you construct the same Oak as Fantomas would. *) // You typically make some helper functions along the way -let mkCodeFromExpression (e: SynExpr) : string = - ParsedInput.ImplFile( - ParsedImplFileInput( - "filename.fsx", - true, - QualifiedNameOfFile(Ident("", Range.Zero)), - [], - [], - [ SynModuleOrNamespace( - [], - false, - SynModuleOrNamespaceKind.AnonModule, - [ SynModuleDecl.Expr(e, Range.Zero) ], - PreXmlDoc.Empty, - [], - None, - Range.Zero, - { LeadingKeyword = SynModuleOrNamespaceLeadingKeyword.None } - ) ], - (false, false), - { ConditionalDirectives = [] - CodeComments = [] }, - Set.empty - ) - ) - |> CodeFormatter.FormatASTAsync - |> Async.RunSynchronously +let text v = SingleTextNode(v, Range.Zero) -let numberExpr = SynExpr.Const(SynConst.Int32(7), Range.Zero) -let wrappedNumber = SynExpr.Paren(numberExpr, Range.Zero, None, Range.Zero) - -try - mkCodeFromExpression wrappedNumber -with _ex -> - // Fantomas.Core will make assumptions about certain constructs. - // Just because you can instantiate the AST, does not mean it will be lead to valid code. - "Code could not be transformed internally" -(*** include-it ***) +let mkCodeFromExpression (e: Expr) = + Oak([], [ ModuleOrNamespaceNode(None, [ ModuleDecl.DeclExpr e ], Range.Zero) ], Range.Zero) + |> CodeFormatter.FormatOakAsync + |> Async.RunSynchronously + |> printfn "%s" -(** -Notice that last but one argument `None`, it represents the range of the closing `)`. -The F# parser would include `Some range` when it parses code, so you need to provide a `Some range` value as well. -Even though the range is empty. Fantomas is designed to work with AST created by the parser. -Creating a `SynExpr.Paren` node is not enough to get both parentheses! -The `CodeFormatter.FormatASTAsync` API is really a side-effect and not a first class citizen. -It will work when you play ball with the exact shape of the parser. -*) +let numberExpr = Expr.Constant(Constant.FromText(text "7")) -let betterWrappedNumber = - SynExpr.Paren(numberExpr, Range.Zero, Some Range.Zero, Range.Zero) +let wrappedNumber = + Expr.Paren(ExprParenNode(text "(", numberExpr, text ")", Range.Zero)) -mkCodeFromExpression betterWrappedNumber -(*** include-it ***) +mkCodeFromExpression wrappedNumber +(*** include-output ***) (** As a rule of thumb: **create what the parser creates, use the online tool!** -Just because you can create AST nodes, does not mean Fantomas will do the right thing. +Just because you can create Oak nodes, does not mean Fantomas will do the right thing. ### Look at the Fantomas code base @@ -216,107 +161,55 @@ For example creating [SynExpr.Lambda](../../reference/fsharp-compiler-syntax-syn When you want to construct `fun a b -> a + b`, the AST the online tool produces looks like: ```fsharp -Lambda - (false, false, - SimplePats - ([Id (a, None, false, false, false, tmp.fsx (1,4--1,5))], - tmp.fsx (1,4--1,5)), - Lambda - (false, true, - SimplePats - ([Id (b, None, false, false, false, tmp.fsx (1,6--1,7))], - tmp.fsx (1,6--1,7)), - App - (NonAtomic, false, - App - (NonAtomic, true, - LongIdent - (false, - SynLongIdent - ([op_Addition], [], [Some (OriginalNotation "+")]), - None, tmp.fsx (1,13--1,14)), Ident a, - tmp.fsx (1,11--1,14)), Ident b, tmp.fsx (1,11--1,16)), - None, tmp.fsx (1,0--1,16), - { ArrowRange = Some tmp.fsx (1,8--1,10) }), - Some - ([Named (SynIdent (a, None), false, None, tmp.fsx (1,4--1,5)); - Named (SynIdent (b, None), false, None, tmp.fsx (1,6--1,7))], - App - (NonAtomic, false, - App - (NonAtomic, true, - LongIdent - (false, - SynLongIdent - ([op_Addition], [], [Some (OriginalNotation "+")]), - None, tmp.fsx (1,13--1,14)), Ident a, - tmp.fsx (1,11--1,14)), Ident b, tmp.fsx (1,11--1,16))), - tmp.fsx (1,0--1,16), { ArrowRange = Some tmp.fsx (1,8--1,10) }) +Oak (1,0-1,16) + ModuleOrNamespaceNode (1,0-1,16) + ExprLambdaNode (1,0-1,16) + "fun" (1,0-1,3) + PatNamedNode (1,4-1,5) + "a" (1,4-1,5) + PatNamedNode (1,6-1,7) + "b" (1,6-1,7) + "->" (1,8-1,10) + ExprInfixAppNode (1,11-1,16) + "a" (1,11-1,12) + "+" (1,13-1,14) + "b" (1,15-1,16) ``` -but the Fantomas `CodePrinter` does not use all this data. -We can easily create a `Lambda` without the nested body structure, as Fantomas will use the `parsedData` information. *) -// this dummy expr will never be used! -let dummyExpr = SynExpr.Const(SynConst.Unit, Range.Zero) let lambdaExpr = - let args = - [ SynPat.Named(SynIdent(Ident("a", Range.Zero), None), false, None, Range.Zero) - SynPat.Named(SynIdent(Ident("b", Range.Zero), None), false, None, Range.Zero) ] - - let expr = - SynExpr.App( - ExprAtomicFlag.NonAtomic, - false, - SynExpr.App( - ExprAtomicFlag.NonAtomic, - true, - SynExpr.LongIdent( - false, - SynLongIdent( - [ Ident("_actually_not_used_", Range.Zero) ], - [], - [ Some(IdentTrivia.OriginalNotation("+")) ] - ), - None, - Range.Zero - - ), - SynExpr.Ident(Ident("a", Range.Zero)), - Range.Zero - ), - SynExpr.Ident(Ident("b", Range.Zero)), - Range.Zero - ) - - SynExpr.Lambda( - false, - false, - SynSimplePats.SimplePats([], Range.Zero), // not used - dummyExpr, // not used - Some(args, expr), // The good stuff is in here! - Range.Zero, - { ArrowRange = Some Range.Zero } + let body: Expr = + ExprInfixAppNode(Expr.Ident(text "a"), text "+", Expr.Ident(text "b"), Range.Zero) + |> Expr.InfixApp + + ExprLambdaNode( + text "fun", + [ Pattern.Named(PatNamedNode(None, text "a", Range.Zero)) + Pattern.Named(PatNamedNode(None, text "b", Range.Zero)) ], + text "->", + body, + Range.Zero ) + |> Expr.Lambda mkCodeFromExpression lambdaExpr -(*** include-it ***) +(*** include-output ***) -(** -Notice how minimal the AST is, versus to what the parser produced. A subset of the data was enough. -How to know which nodes to include? Take a look at `CodePrinter.fs` and `SourceParser.fs`! +(** +How to know which nodes to include? Take a look at `CodePrinter.fs`! ### Create your own set of helper functions Throughout all these examples, we have duplicated a lot of code. You can typically easily refactor this into some helper functions. The Fantomas maintainers are not affiliated with any projects that expose AST construction helpers. -Relying on these projects, is at your own risk. The constructed AST might not be suitable for what Fantomas expects. ### Updates -Since code generation is considered to be a nice to have functionality, there is no compatibility between any `Fantomas.FCS`. -We do not apply any semantic versioning to `Fantomas.FCS`. Breaking changes can be expected at any given point. +Since code generation is considered to be a nice to have functionality, there is no compatibility between any `Fantomas.Core` version when it comes to the `SyntaxOak` module. +We do not apply any semantic versioning to `Fantomas.FCS` or `Fantomas.Core.SyntaxOak`. Breaking changes can be expected at any given point. +Our recommendation is that you include a set of regression tests to meet your own expectations when upgrading. +As none of our versions are compatible it is advised to take a very strict dependency on `Fantomas.Core`. Using constraints like `(>= 6.0.0)` will inevitably lead to unexpected problems. - + *) diff --git a/docs/docs/end-users/Rider.md b/docs/docs/end-users/Rider.md index 3d77eba68b..bf97231beb 100644 --- a/docs/docs/end-users/Rider.md +++ b/docs/docs/end-users/Rider.md @@ -46,7 +46,7 @@ fsharp_array_or_list_multiline_formatter=character_width fsharp_max_value_binding_width=80 fsharp_max_function_binding_width=40 fsharp_max_dot_get_expression_width=80 -fsharp_multiline_block_brackets_on_same_column=false +fsharp_multiline_bracket_style = cramped fsharp_newline_between_type_definition_and_members=true fsharp_align_function_signature_to_indentation=false fsharp_alternative_long_member_definitions=false @@ -54,7 +54,6 @@ fsharp_multi_line_lambda_closing_newline=false fsharp_experimental_keep_indent_in_branch=false fsharp_blank_lines_around_nested_multiline_expressions=true fsharp_bar_before_discriminated_union_declaration=false -fsharp_experimental_stroustrup_style=false fsharp_keep_max_number_of_blank_lines=100 fsharp_strict_mode=false ``` diff --git a/docs/docs/end-users/UpgradeGuide.md b/docs/docs/end-users/UpgradeGuide.md new file mode 100644 index 0000000000..8847d0ca07 --- /dev/null +++ b/docs/docs/end-users/UpgradeGuide.md @@ -0,0 +1,80 @@ +--- +category: End-users +categoryindex: 1 +index: 12 +--- +# Upgrade guide + +We wish to capture all changes required to upgrade to a new version. Please note that the focus of this document is about how to upgrade. +New features are not covered in detail here, for those please refer to our [changelog](https://github.com/fsprojects/fantomas/blob/main/CHANGELOG.md). +If you find something to be missing from this guide, please consider opening a PR to mend the gap instead of opening an issue. + + + +## v5.0 + +### .editorconfig + +- `fsharp_max_elmish_width` was removed. +- `fsharp_single_argument_web_mode` was removed. +- `fsharp_disable_elmish_syntax` was removed. +- `fsharp_semicolon_at_end_of_line` was removed. +- `fsharp_keep_if_then_in_same_line` was removed. +- `fsharp_indent_on_try_with` was removed. +- If you were using Elmish inspired code (or `fsharp_single_argument_web_mode`) use + +``` +fsharp_multiline_block_brackets_on_same_column = true +fsharp_experimental_stroustrup_style = true +``` +- `fsharp_keep_indent_in_branch ` was renamed to `fsharp_experimental_keep_indent_in_branch` + +### console application + +- The dotnet tool is now targeting `net6.0`. +- `--stdin` was removed. +- `--stdout` was removed. +- `--fsi` was removed. +- `--force` now writes a formatted file to disk, regardless of its validity. + +### Miscellaneous + +- NuGet package `Fantomas` was renamed to `Fantomas.Core`. +- NuGet package `fantomas-tool` was renamed to `fantomas`. +- `Fantomas.Core` uses [Fantomas.FCS](https://www.nuget.org/packages/Fantomas.FCS) instead of [FSharp.Compiler.Service](https://www.nuget.org/packages/FSharp.Compiler.Service) +- NuGet package `Fantomas.Extras` is deprecated. + +## v5.1 + +### .editorconfig + +- The space in patterns is no longer controlled by `fsharp_space_before_parameter`, + `fsharp_space_before_lowercase_invocation` and `fsharp_space_before_uppercase_invocation` are now used. + +## v5.2 + +### .editorconfig + +- `fsharp_multiline_block_brackets_on_same_column` and `fsharp_experimental_stroustrup_style` are now merged into one setting `fsharp_multiline_bracket_style`. + The accepted values for `fsharp_multiline_bracket_style` are `cramped`, `aligned` and `experimental_stroustrup`.
+ Note that `fsharp_multiline_block_brackets_on_same_column` and `fsharp_experimental_stroustrup_style` will continue to work until the next major version. + +## v6.0 (latest alpha) + +### .editorconfig + +- `fsharp_multiline_block_brackets_on_same_column` and `fsharp_experimental_stroustrup_style` are replaced with `fsharp_multiline_bracket_style` +- `experimental_stroustrup` for `fsharp_multiline_bracket_style` is now `stroustrup` +- `fsharp_newline_before_multiline_computation_expression` was extracted from `fsharp_multiline_bracket_style = stroustrup` and now controls how computation expression behave. +- `fsharp_strict_mode` was removed and can no longer be used. + +### console application +- `-v` is now short for `--verbosity` instead of `--version` +- The console output was revamped. +- `--recurse` was removed. Please use [.fantomasignore](./IgnoreFiles.html) file if you wish to ignore certain files. + +### Miscellaneous +- The public API of CodeFormatter no longer uses `FSharpOption<'T>`, instead overloads are now used. +- `StrictMode` was removed from `FormatConfig`, not passing the source text in the public API will have the same effect. + + diff --git a/docs/images/ast-viewer.png b/docs/images/ast-viewer.png deleted file mode 100644 index 07b6ef7e3e..0000000000 Binary files a/docs/images/ast-viewer.png and /dev/null differ diff --git a/docs/images/oak-viewer.png b/docs/images/oak-viewer.png new file mode 100644 index 0000000000..2eefd84000 Binary files /dev/null and b/docs/images/oak-viewer.png differ diff --git a/docs/images/searchbar-ast.png b/docs/images/searchbar-ast.png index d44c129ac6..6b18ecbb82 100644 Binary files a/docs/images/searchbar-ast.png and b/docs/images/searchbar-ast.png differ diff --git a/fantomas.sln b/fantomas.sln index ba8edd6db9..455820b02b 100644 --- a/fantomas.sln +++ b/fantomas.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{29F22904-C ProjectSection(SolutionItems) = preProject docs\index.html = docs\index.html docs\.README.md = docs\.README.md + docs\docs\end-users\Configuration.fsx = docs\docs\end-users\Configuration.fsx EndProjectSection EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fantomas.Client", "src\Fantomas.Client\Fantomas.Client.fsproj", "{AA895F94-CCF2-4FCF-A9BB-E16987B57535}" @@ -40,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fantomas.Client.Tests", "src\Fantomas.Client.Tests\Fantomas.Client.Tests.fsproj", "{68814E36-3957-4D1C-BCDB-84C3C8478BEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +137,18 @@ Global {B39D50EE-0307-4C08-81F5-97418A946F63}.Release|x64.Build.0 = Release|Any CPU {B39D50EE-0307-4C08-81F5-97418A946F63}.Release|x86.ActiveCfg = Release|Any CPU {B39D50EE-0307-4C08-81F5-97418A946F63}.Release|x86.Build.0 = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|x64.ActiveCfg = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|x64.Build.0 = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|x86.ActiveCfg = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Debug|x86.Build.0 = Debug|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|Any CPU.Build.0 = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|x64.ActiveCfg = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|x64.Build.0 = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|x86.ActiveCfg = Release|Any CPU + {68814E36-3957-4D1C-BCDB-84C3C8478BEC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Fantomas.Benchmarks/Runners.fs b/src/Fantomas.Benchmarks/Runners.fs index f121ac5802..482fe4fe97 100644 --- a/src/Fantomas.Benchmarks/Runners.fs +++ b/src/Fantomas.Benchmarks/Runners.fs @@ -4,7 +4,7 @@ open System.IO open BenchmarkDotNet.Attributes open Fantomas.Core -let config = FormatConfig.FormatConfig.Default +let config = FormatConfig.Default [] [] diff --git a/src/Fantomas.Client.Tests/EndToEndTests.fs b/src/Fantomas.Client.Tests/EndToEndTests.fs new file mode 100644 index 0000000000..bd9dd604ae --- /dev/null +++ b/src/Fantomas.Client.Tests/EndToEndTests.fs @@ -0,0 +1,107 @@ +module Fantomas.Client.Tests + +open System +open System.IO +open System.Threading.Tasks +open CliWrap +open CliWrap.Buffered +open Fantomas.Client.Contracts +open Fantomas.Client.LSPFantomasService +open Fantomas.Client.LSPFantomasServiceTypes +open NUnit.Framework + +[] +type EndToEndTests() = + let folder: DirectoryInfo = + DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))) + + let service: FantomasService = new LSPFantomasService() + + let unformattedCode = "let a = 8" + + let withVersion version (callback: string -> Task) = + if Path.Exists(Path.Combine(folder.FullName, version)) then + backgroundTask { + let file = Path.Combine(folder.FullName, version, "File.fs") + do! callback file + } + else + backgroundTask { + let subDirectory = folder.CreateSubdirectory(version) + + let dotnet (command: string) = + Cli + .Wrap("dotnet") + .WithWorkingDirectory(subDirectory.FullName) + .WithArguments(command) + .ExecuteBufferedAsync() + .Task + :> Task + + // This sdk version must match the version used in this repository. + // It will be the version which the CI/CD pipeline has access to. + do! dotnet "new globaljson --sdk-version 7.0.100 --roll-forward latestPatch" + do! dotnet "new tool-manifest" + + do! + dotnet + $"tool install fantomas -v d --version {version} --add-source https://api.nuget.org/v3/index.json" + + let fsharpFile = Path.Combine(subDirectory.FullName, "File.fs") + File.Create(fsharpFile).Dispose() + do! callback fsharpFile + } + + [] + member _.Setup() = folder.Create() + + [] + member _.TearDown() = + backgroundTask { + service.Dispose() + // Give it a little time before all processes are truly killed. + do! Task.Delay(200) + folder.Delete(true) + } + + [] + [] + [] + [] + member _.Version(version: string) = + withVersion version (fun fsharpFile -> + backgroundTask { + let! version = service.VersionAsync(fsharpFile) + Assert.AreEqual(int FantomasResponseCode.Version, version.Code) + }) + + [] + [] + [] + [] + member _.FormatDocument(version: string) = + withVersion version (fun fsharpFile -> + backgroundTask { + let request: FormatDocumentRequest = + { SourceCode = unformattedCode + FilePath = fsharpFile + Config = None + Cursor = None } + + let! formatResponse = service.FormatDocumentAsync(request) + Assert.AreEqual(int FantomasResponseCode.Formatted, formatResponse.Code) + }) + + [] + member _.``FormatDocument with Cursor``(version: string) = + withVersion version (fun fsharpFile -> + backgroundTask { + let request: FormatDocumentRequest = + { SourceCode = unformattedCode + FilePath = fsharpFile + Config = None + Cursor = Some(FormatCursorPosition(1, 12)) } + + let! formatResponse = service.FormatDocumentAsync(request) + Assert.AreEqual(int FantomasResponseCode.Formatted, formatResponse.Code) + }) diff --git a/src/Fantomas.Client.Tests/Fantomas.Client.Tests.fsproj b/src/Fantomas.Client.Tests/Fantomas.Client.Tests.fsproj new file mode 100644 index 0000000000..c6b2d92df6 --- /dev/null +++ b/src/Fantomas.Client.Tests/Fantomas.Client.Tests.fsproj @@ -0,0 +1,27 @@ + + + + FS0988 + net7.0 + false + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/Fantomas.Client.Tests/packages.lock.json b/src/Fantomas.Client.Tests/packages.lock.json new file mode 100644 index 0000000000..7c5c66f44b --- /dev/null +++ b/src/Fantomas.Client.Tests/packages.lock.json @@ -0,0 +1,819 @@ +{ + "version": 1, + "dependencies": { + "net7.0": { + "CliWrap": { + "type": "Direct", + "requested": "[3.6.0, )", + "resolved": "3.6.0", + "contentHash": "AY6LvRZOEYuAiuaWPLnIDddJUnpiPpiSvfoPwweEXI1orRNnsAwf6sOv9Tt0J4GFrlwejFF/INuR57iEKIh7bw==" + }, + "FSharp.Core": { + "type": "Direct", + "requested": "[6.0.1, )", + "resolved": "6.0.1", + "contentHash": "VrFAiW8dEEekk+0aqlbvMNZzDvYXmgWZwAt68AUBqaWK8RnoEVUNglj66bZzhs4/U63q0EfXlhcEKnH1sTYLjw==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.3.2, )", + "resolved": "17.3.2", + "contentHash": "apR0ha1T8FujBwq1P8i/DOZjbI5XhcP/i8As4NnVztVSpZG8GtWRPCstcmgkUkBpvEfcrrDPlJWbuZY+Hl1hSg==", + "dependencies": { + "Microsoft.CodeCoverage": "17.3.2", + "Microsoft.TestPlatform.TestHost": "17.3.2" + } + }, + "NUnit": { + "type": "Direct", + "requested": "[3.13.3, )", + "resolved": "3.13.3", + "contentHash": "KNPDpls6EfHwC3+nnA67fh5wpxeLb3VLFAfLxrug6JMYDLHH6InaQIWR7Sc3y75d/9IKzMksH/gi08W7XWbmnQ==", + "dependencies": { + "NETStandard.Library": "2.0.0" + } + }, + "NUnit3TestAdapter": { + "type": "Direct", + "requested": "[4.2.1, )", + "resolved": "4.2.1", + "contentHash": "kgH8VKsrcZZgNGQXRpVCrM7TnNz9li3b/snH+YmnXUNqsaWa1Xw9EQWHpbzq4Li2FbTjTE/E5N5HdLNXzZ8BpQ==" + }, + "NunitXml.TestLogger": { + "type": "Direct", + "requested": "[3.0.127, )", + "resolved": "3.0.127", + "contentHash": "v8cEbYVSZGwCD6290yKeRCsRpOwYcgnng1YRrQKGgP79mHuxj1b1lpb6kA02QoLUM5/Qv9aDSLmS0U05m6HlJg==" + }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.2.85", + "contentHash": "3SqAgwNV5LOf+ZapHmjQMUc7WDy/1ur9CfFNjgnfMZKCB5CxkVVbyHa06fObjGTEHZI7mcDathYjkI+ncr92ZQ==", + "dependencies": { + "MessagePack.Annotations": "2.2.85", + "Microsoft.Bcl.AsyncInterfaces": "1.0.0", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Emit": "4.6.0", + "System.Reflection.Emit.Lightweight": "4.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.2.85", + "contentHash": "YptRsDCQK35K5FhmZ0LojW4t8I6DpetLfK5KG8PVY2f6h7/gdyr8f4++xdSEK/xS6XX7/GPvEpqszKVPksCsiQ==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.3.2", + "contentHash": "+CeYNY9hYNRgv1wAID5koeDVob1ZOrOYfRRTLxU9Zm5ZMDMkMZ8wzXgakxVv+jtk8tPdE8Ze9vVE+czMKapv/Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.3.2", + "contentHash": "DJEIfSA2GDC+2m42vKGNR2hm+Uhta4SpCsLZVVvYIiYMjxtk7GzNnv82qvE4SCW3kIYllMg2D0rr8juuj/f7AA==", + "dependencies": { + "NuGet.Frameworks": "5.11.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.3.2", + "contentHash": "113J19v31pIx+PzmdEw67cWTZWh/YApnprbclFeat6szNbnpKOKG7Ap4PX5LT6E5Da+xONyilxvx2HZPpEaXPQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.3.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "Microsoft.VisualStudio.Threading": { + "type": "Transitive", + "resolved": "16.9.60", + "contentHash": "9igpltD4NDMb1QeLiuAShr4inAG/MEm/GL0VE3tCUXQmwrfrbrmwrhAn5fXy2uiZ1g2s2qSUkyEvx7sp2h7M8Q==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.VisualStudio.Threading.Analyzers": "16.9.60", + "Microsoft.VisualStudio.Validation": "16.8.33", + "Microsoft.Win32.Registry": "5.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "16.9.60", + "contentHash": "kbl+ra5Ao93lDar3A2vUSdfWiHMYBBsLM3Z6i/t6fH2iPHGyMTqvt3z20XCZ+L+1gcc8lpbhmkFS4rh+zwfsTg==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "16.8.33", + "contentHash": "onzrXL8gsjht1knmmViGLTU3l1LIKoVLDL+gLN9Pdd+gclED9jLgxx/5X3mJHqETHMi7Va//hNCekiJ11LezSg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Nerdbank.Streams": { + "type": "Transitive", + "resolved": "2.6.81", + "contentHash": "htBHFE359qyyFwrvAGvFxrbBAoldZdl0XjtQdDWTJ8t5sWWs7QVXID5y1ZGJE61UgpV5CqWsj/NT0LOAn5GdZw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "Microsoft.VisualStudio.Threading": "16.7.56", + "Microsoft.VisualStudio.Validation": "15.5.31", + "System.IO.Pipelines": "4.7.2", + "System.Net.WebSockets": "4.3.0", + "System.Runtime.CompilerServices.Unsafe": "4.7.1" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "12.0.2", + "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "5.11.0", + "contentHash": "eaiXkUjC4NPcquGWzAGMXjuxvLwc6XGKMptSyOGQeT0X70BUZObuybJFZLA0OfTdueLd3US23NBPTBb6iF3V1Q==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" + }, + "SemanticVersioning": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "4EQgYdNZ92SyaO7YFk6olVnebF5V+jrHyMUjvPq89tLeMo8NSfgDF+6Zwq/lgh9j/0yfQp9Lkm0ZA0rUATCZFA==" + }, + "StreamJsonRpc": { + "type": "Transitive", + "resolved": "2.8.28", + "contentHash": "i2hKUXJSLEoWpPqQNyISqLDqmFHMiyasjTC/PrrHNWhQyauFeVoebSct3E4OTUzRC1DYjVJ9AMiVbp/uVYLnjQ==", + "dependencies": { + "MessagePack": "2.2.85", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.VisualStudio.Threading": "16.9.60", + "Nerdbank.Streams": "2.6.81", + "Newtonsoft.Json": "12.0.2", + "System.Collections.Immutable": "5.0.0", + "System.Diagnostics.DiagnosticSource": "5.0.1", + "System.IO.Pipelines": "5.0.1", + "System.Memory": "4.5.4", + "System.Net.Http": "4.3.4", + "System.Net.WebSockets": "4.3.0", + "System.Reflection.Emit": "4.7.0", + "System.Threading.Tasks.Dataflow": "5.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==" + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.1", + "contentHash": "uXQEYqav2V3zP6OwkOKtLv+qIi6z3m1hsGyKwXX7ZA7htT4shoVccGxnJ9kVRFPNAsi1ArZTq2oh7WOto6GbkQ==" + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "u6fFNY5q4T8KerUAVbya7bR6b7muBuSTAersyrihkcmE5QhEOiH3t5rh4il15SexbVlpXFHGuMwr/m8fDrnkQg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" + }, + "fantomas.client": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.1, )", + "SemanticVersioning": "[2.0.2, )", + "StreamJsonRpc": "[2.8.28, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Fantomas.Client/CHANGELOG.md b/src/Fantomas.Client/CHANGELOG.md index fcd5f26be0..142bc3a776 100644 --- a/src/Fantomas.Client/CHANGELOG.md +++ b/src/Fantomas.Client/CHANGELOG.md @@ -2,6 +2,12 @@ This is the changelog for the Fantomas.Client package specifically. It's distinct from that of the overall libraries and command-line tool. +## 0.9.0 - 2023-02-24 +* Fix JSON serialization of new cursor API. [#2778](https://github.com/fsprojects/fantomas/issues/2778) + +## 0.8.0 - 2023-01-24 +* Initial cursor API. [#2739](https://github.com/fsprojects/fantomas/pull/2739) + ## 0.7.0 - 2022-11-09 ### Changed diff --git a/src/Fantomas.Client/Contracts.fs b/src/Fantomas.Client/Contracts.fs index 17016b9bd4..9de7f8a1c1 100644 --- a/src/Fantomas.Client/Contracts.fs +++ b/src/Fantomas.Client/Contracts.fs @@ -20,17 +20,21 @@ module Methods = let Configuration = "fantomas/configuration" type FormatDocumentRequest = - { - SourceCode: string - /// File path will be used to identify the .editorconfig options - /// Unless the configuration is passed - FilePath: string - /// Overrides the found .editorconfig. - Config: IReadOnlyDictionary option - } + { SourceCode: string + FilePath: string + Config: IReadOnlyDictionary option + Cursor: FormatCursorPosition option } member this.IsSignatureFile = this.FilePath.EndsWith(".fsi") +and FormatCursorPosition = + class + val Line: int + val Column: int + + new(line: int, column: int) = { Line = line; Column = column } + end + type FormatSelectionRequest = { SourceCode: string @@ -60,14 +64,11 @@ and FormatSelectionRange = end type FantomasResponse = - { - Code: int - FilePath: string - Content: string option - /// The actual range that was used to format a selection. - /// This can differ from the input selection range if the selection had leading or trailing whitespace. - SelectedRange: FormatSelectionRange option - } + { Code: int + FilePath: string + Content: string option + SelectedRange: FormatSelectionRange option + Cursor: FormatCursorPosition option } type FantomasService = interface diff --git a/src/Fantomas.Client/Contracts.fsi b/src/Fantomas.Client/Contracts.fsi index a0afbbde0a..c94e5a73dc 100644 --- a/src/Fantomas.Client/Contracts.fsi +++ b/src/Fantomas.Client/Contracts.fsi @@ -28,10 +28,21 @@ type FormatDocumentRequest = /// Overrides the found .editorconfig. Config: IReadOnlyDictionary option + + /// The current position of the cursor. + /// Zero-based + Cursor: FormatCursorPosition option } member IsSignatureFile: bool +and FormatCursorPosition = + class + new: line: int * column: int -> FormatCursorPosition + val Line: int + val Column: int + end + type FormatSelectionRequest = { SourceCode: string @@ -67,6 +78,10 @@ type FantomasResponse = /// The actual range that was used to format a selection. /// This can differ from the input selection range if the selection had leading or trailing whitespace. SelectedRange: FormatSelectionRange option + + /// Cursor position after formatting. + /// Zero-based. + Cursor: FormatCursorPosition option } type FantomasService = diff --git a/src/Fantomas.Client/LSPFantomasService.fs b/src/Fantomas.Client/LSPFantomasService.fs index 575c62428f..b99dbf3e1b 100644 --- a/src/Fantomas.Client/LSPFantomasService.fs +++ b/src/Fantomas.Client/LSPFantomasService.fs @@ -4,6 +4,7 @@ open System open System.IO open System.Threading open System.Threading.Tasks +open Newtonsoft.Json.Linq open StreamJsonRpc open Fantomas.Client.Contracts open Fantomas.Client.LSPFantomasServiceTypes @@ -167,14 +168,16 @@ let private fileNotFoundResponse filePath : Task = { Code = int FantomasResponseCode.FileNotFound FilePath = filePath Content = Some $"File \"%s{filePath}\" does not exist." - SelectedRange = None } + SelectedRange = None + Cursor = None } |> Task.FromResult let private fileNotAbsoluteResponse filePath : Task = { Code = int FantomasResponseCode.FilePathIsNotAbsolute FilePath = filePath Content = Some $"\"%s{filePath}\" is not an absolute file path. Relative paths are not supported." - SelectedRange = None } + SelectedRange = None + Cursor = None } |> Task.FromResult let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task = @@ -214,14 +217,16 @@ let private daemonNotFoundResponse filePath (error: GetDaemonError) : Task Task.FromResult let private cancellationWasRequestedResponse filePath : Task = { Code = int FantomasResponseCode.CancellationWasRequested FilePath = filePath Content = Some "FantomasService is being or has been disposed." - SelectedRange = None } + SelectedRange = None + Cursor = None } |> Task.FromResult let mapResultToResponse (filePath: string) (result: Result, FantomasServiceError>) = @@ -232,6 +237,102 @@ let mapResultToResponse (filePath: string) (result: Result daemonNotFoundResponse filePath e | Error FantomasServiceError.CancellationWasRequested -> cancellationWasRequestedResponse filePath +/// +/// +/// The Fantomas daemon currently sends a Fantomas.Client.LSPFantomasServiceTypes.FormatDocumentResponse back to Fantomas.Client. +/// This was a poor choice as the serialization of a DU case breaks when you add a new field to it. Even though that field is optional. +/// To overcome this, we deserialize the FormatDocumentResponse ourselves to construct the matching FantomasResponse. +/// +/// +/// In v6.0 we introduced an additional option field to FormatDocumentResponse.Formatted being the cursor position. +/// That is why we currently have two match cases that try to deserialize "Formatted". +/// +/// +/// When serialization fails, we re-use the input file path from the request information. +/// The raw JObject that send sent over the wire. +let decodeFormatResult (inputFilePath: string) (json: JObject) : FantomasResponse = + let mkError msg = + { Code = int FantomasResponseCode.Error + FilePath = inputFilePath + Content = Some msg + SelectedRange = None + Cursor = None } + + try + if not (json.ContainsKey("Case")) || not (json.ContainsKey("Fields")) then + mkError "Expected \"Case\" and \"Fields\" to be present in the response json" + else + let caseName = json.["Case"].Value() + let fields = json.["Fields"].Value() + + match caseName with + | "Formatted" when fields.Count = 2 -> + let fileName = fields.[0].Value() + let formattedContent = fields.[1].Value() + + { Code = int FantomasResponseCode.Formatted + FilePath = fileName + Content = Some formattedContent + SelectedRange = None + Cursor = None } + | "Formatted" when fields.Count = 3 -> + let fileName = fields.[0].Value() + let formattedContent = fields.[1].Value() + + let cursor = + if fields.[2].Type = JTokenType.Null then + None + else + // This is wrapped as an option, the Case is "Some" here. + // We need to extract the Line and Column from the first item in Fields + let cursorObject = fields.[2].Value() + let cursorObject = cursorObject.["Fields"].[0].Value() + + Some( + FormatCursorPosition( + cursorObject.["Line"].Value(), + cursorObject.["Column"].Value() + ) + ) + + { Code = int FantomasResponseCode.Formatted + FilePath = fileName + Content = Some formattedContent + SelectedRange = None + Cursor = cursor } + + | "Unchanged" when fields.Count = 1 -> + let fileName = fields.[0].Value() + + { Code = int FantomasResponseCode.UnChanged + FilePath = fileName + Content = None + SelectedRange = None + Cursor = None } + | "Error" when fields.Count = 2 -> + let fileName = fields.[0].Value() + let formattingError = fields.[1].Value() + + { Code = int FantomasResponseCode.Error + FilePath = fileName + Content = Some formattingError + SelectedRange = None + Cursor = None } + | "IgnoredFile" when fields.Count = 1 -> + let fileName = fields.[0].Value() + + { Code = int FantomasResponseCode.Ignored + FilePath = fileName + Content = None + SelectedRange = None + Cursor = None } + | _ -> + mkError + $"Could not deserialize the message from the daemon, got unexpected case name %s{caseName} with %i{fields.Count} fields." + + with ex -> + mkError $"Could not deserialize the message from the daemon, %s{ex.Message}" + type LSPFantomasService() = let cts = new CancellationTokenSource() let agent = createAgent cts.Token @@ -239,7 +340,7 @@ type LSPFantomasService() = interface FantomasService with member this.Dispose() = if not cts.IsCancellationRequested then - agent.PostAndReply Reset + let _ = agent.PostAndReply Reset cts.Cancel() member _.VersionAsync(filePath, ?cancellationToken: CancellationToken) : Task = @@ -256,7 +357,8 @@ type LSPFantomasService() = { Code = int FantomasResponseCode.Version Content = Some t.Result FilePath = filePath - SelectedRange = None })) + SelectedRange = None + Cursor = None })) |> mapResultToResponse filePath member _.FormatDocumentAsync @@ -269,12 +371,12 @@ type LSPFantomasService() = |> Result.bind (getDaemon agent) |> Result.map (fun client -> client - .InvokeWithParameterObjectAsync( + .InvokeWithParameterObjectAsync( Methods.FormatDocument, argument = formatDocumentOptions, cancellationToken = Option.defaultValue cts.Token cancellationToken ) - .ContinueWith(fun (t: Task) -> t.Result.AsFormatResponse())) + .ContinueWith(fun (t: Task) -> decodeFormatResult formatDocumentOptions.FilePath t.Result)) |> mapResultToResponse formatDocumentOptions.FilePath member _.FormatSelectionAsync @@ -310,7 +412,8 @@ type LSPFantomasService() = { Code = int FantomasResponseCode.Configuration FilePath = filePath Content = Some t.Result - SelectedRange = None })) + SelectedRange = None + Cursor = None })) |> mapResultToResponse filePath member _.ClearCache() = agent.PostAndReply Reset diff --git a/src/Fantomas.Client/LSPFantomasServiceTypes.fs b/src/Fantomas.Client/LSPFantomasServiceTypes.fs index a0f3376acb..7505e3b22d 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fs +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fs @@ -29,43 +29,22 @@ type FormatSelectionResponse = { Code = int FantomasResponseCode.Formatted FilePath = name Content = Some content - SelectedRange = Some formattedRange } + SelectedRange = Some formattedRange + Cursor = None } | FormatSelectionResponse.Error(name, ex) -> { Code = int FantomasResponseCode.Error FilePath = name Content = Some ex - SelectedRange = None } + SelectedRange = None + Cursor = None } [] type FormatDocumentResponse = - | Formatted of filename: string * formattedContent: string + | Formatted of filename: string * formattedContent: string * cursor: FormatCursorPosition option | Unchanged of filename: string | Error of filename: string * formattingError: string | IgnoredFile of filename: string - member this.AsFormatResponse() = - match this with - | FormatDocumentResponse.Formatted(name, content) -> - { Code = int FantomasResponseCode.Formatted - FilePath = name - Content = Some content - SelectedRange = None } - | FormatDocumentResponse.Unchanged name -> - { Code = int FantomasResponseCode.UnChanged - FilePath = name - Content = None - SelectedRange = None } - | FormatDocumentResponse.Error(name, err) -> - { Code = int FantomasResponseCode.Error - FilePath = name - Content = Some(err) - SelectedRange = None } - | FormatDocumentResponse.IgnoredFile name -> - { Code = int FantomasResponseCode.Ignored - FilePath = name - Content = None - SelectedRange = None } - type FantomasVersion = FantomasVersion of string type FantomasExecutableFile = FantomasExecutableFile of string type Folder = Folder of path: string diff --git a/src/Fantomas.Client/LSPFantomasServiceTypes.fsi b/src/Fantomas.Client/LSPFantomasServiceTypes.fsi index 075430f02e..8dedde8b4e 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fsi +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fsi @@ -24,13 +24,11 @@ type FormatSelectionResponse = [] type FormatDocumentResponse = - | Formatted of filename: string * formattedContent: string + | Formatted of filename: string * formattedContent: string * cursor: FormatCursorPosition option | Unchanged of filename: string | Error of filename: string * formattingError: string | IgnoredFile of filename: string - member AsFormatResponse: unit -> FantomasResponse - type FantomasVersion = FantomasVersion of string type FantomasExecutableFile = FantomasExecutableFile of string diff --git a/src/Fantomas.Core.Tests/ASTTransformerTests.fs b/src/Fantomas.Core.Tests/ASTTransformerTests.fs index 4ba6ec85af..1afdff97a3 100644 --- a/src/Fantomas.Core.Tests/ASTTransformerTests.fs +++ b/src/Fantomas.Core.Tests/ASTTransformerTests.fs @@ -6,7 +6,6 @@ open FSharp.Compiler.Xml open FSharp.Compiler.Syntax open FSharp.Compiler.SyntaxTrivia open Fantomas.Core -open Fantomas.Core.FormatConfig [] let ``avoid stack-overflow in long array/list, 2485`` () = diff --git a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs similarity index 98% rename from src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs rename to src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs index 64bc6f0bc3..56f215d4e0 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs @@ -1,9 +1,9 @@ -module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnArrayOrListTests +module Fantomas.Core.Tests.AlignedMultilineBracketStyleArrayOrListTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with diff --git a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs similarity index 90% rename from src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs rename to src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs index 8d89f76448..8752786b19 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs @@ -1,9 +1,9 @@ -module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnRecordTests +module Fantomas.Core.Tests.AlignedMultilineBracketStyleTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with @@ -1182,6 +1182,29 @@ type Foo = static member Baz : int """ +[] +let ``record type definition with members and trivia`` () = + formatSourceString + false + """ +type X = { + Y : int +} with // foo + member x.Z = () +""" + { config with + NewlineBetweenTypeDefinitionAndMembers = false } + |> prepend newline + |> should + equal + """ +type X = + { + Y : int + } // foo + member x.Z = () +""" + [] let ``anonymous records with comments on record fields`` () = formatSourceString @@ -1456,3 +1479,117 @@ let a = // test2 |} """ + +[] +let ``equality comparison with a `with` expression should format correctly with Allman alignment, 2507`` () = + formatSourceString + false + """ +let compareThings (first: Thing) (second: Thing) = + first = { second with + Foo = first.Foo + Bar = first.Bar + } +""" + { config with + MultilineBracketStyle = Aligned } + |> prepend newline + |> should + equal + """ +let compareThings (first : Thing) (second : Thing) = + first = { second with + Foo = first.Foo + Bar = first.Bar + } +""" + +// `Aligned` copy-and-update expression keeps label on first line to match G-Research style guide. +// See https://github.com/G-Research/fsharp-formatting-conventions#formatting-copy-and-update-record-expressions +[] +let ``update record in aligned style`` () = + formatSourceString + false + """ +// standalone +{ rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } + +// binding expression +let v = { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + config + |> prepend newline + |> should + equal + """ +// standalone +{ rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy" ; "George" ; "Bungle" ] +} + +// binding expression +let v = + { rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy" ; "George" ; "Bungle" ] + } +""" + +// In contrast, Stroustrup will indent the entire record body when the record is placed standalone. +[] +let ``update record in stroustrup style`` () = + formatSourceString + false + """ +// standalone +{ rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } + +// binding expression +let v = { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } +""" + { config with + MultilineBracketStyle = Stroustrup } + |> prepend newline + |> should + equal + """ +// standalone +{ + rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy" ; "George" ; "Bungle" ] +} + +// binding expression +let v = { + rainbow with + Boss = "Jeffrey" + Lackeys = [ "Zippy" ; "George" ; "Bungle" ] +} +""" + +[] +let ``anonymous struct record with trivia`` () = + formatSourceString + false + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" + config + |> prepend newline + |> should + equal + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" diff --git a/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs b/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs index 3e127072ac..464a6e64a0 100644 --- a/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs +++ b/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs @@ -3,7 +3,6 @@ module Fantomas.Core.Tests.CodePrinterHelperFunctionsTests open NUnit.Framework open FsUnit open Fantomas.Core.Context -open Fantomas.Core.FormatConfig open Fantomas.Core open Fantomas.Core.SyntaxOak @@ -12,7 +11,7 @@ open Fantomas.Core.SyntaxOak // It might help for some things to "click". /// Transform the WriterEvents in a Context to a string -let private dump (context: Context) : string = dump false context +let private dump (context: Context) : string = (dump false context).Code [] let ``!- add a single WriterEvent.Write`` () = diff --git a/src/Fantomas.Core.Tests/ColMultilineItemTests.fs b/src/Fantomas.Core.Tests/ColMultilineItemTests.fs index 1c5a97b2d5..adfcd6cc42 100644 --- a/src/Fantomas.Core.Tests/ColMultilineItemTests.fs +++ b/src/Fantomas.Core.Tests/ColMultilineItemTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.ColMultilineItemTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``two short let binding should not have extra newline`` () = diff --git a/src/Fantomas.Core.Tests/CommentTests.fs b/src/Fantomas.Core.Tests/CommentTests.fs index eeb5e5cbc3..d79bc43650 100644 --- a/src/Fantomas.Core.Tests/CommentTests.fs +++ b/src/Fantomas.Core.Tests/CommentTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.CommentTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``should keep sticky-to-the-left comments after nowarn directives`` () = diff --git a/src/Fantomas.Core.Tests/ComputationExpressionTests.fs b/src/Fantomas.Core.Tests/ComputationExpressionTests.fs index f8c799bd1c..a0a2b8ed2f 100644 --- a/src/Fantomas.Core.Tests/ComputationExpressionTests.fs +++ b/src/Fantomas.Core.Tests/ComputationExpressionTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.ComputationExpressionTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``async workflows`` () = diff --git a/src/Fantomas.Core.Tests/ContextTests.fs b/src/Fantomas.Core.Tests/ContextTests.fs index b1e15f07ae..8091953711 100644 --- a/src/Fantomas.Core.Tests/ContextTests.fs +++ b/src/Fantomas.Core.Tests/ContextTests.fs @@ -4,10 +4,9 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Context open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig open Fantomas.Core -let private dump = dump false +let private dump ctx = (dump false ctx).Code [] let ``sepSpace should not add an additional space if the line ends with a space`` () = diff --git a/src/Fantomas.Core.Tests/ControlStructureTests.fs b/src/Fantomas.Core.Tests/ControlStructureTests.fs index 6f2cd2ff05..8b4556fe47 100644 --- a/src/Fantomas.Core.Tests/ControlStructureTests.fs +++ b/src/Fantomas.Core.Tests/ControlStructureTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.ControlStructureTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``if/then/else block`` () = diff --git a/src/Fantomas.Core.Tests/RecordTests.fs b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs similarity index 98% rename from src/Fantomas.Core.Tests/RecordTests.fs rename to src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs index 675fcbf368..4d22cff4e6 100644 --- a/src/Fantomas.Core.Tests/RecordTests.fs +++ b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs @@ -1,9 +1,9 @@ -module Fantomas.Core.Tests.RecordTests +module Fantomas.Core.Tests.CrampedMultilineBracketStyleTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``record declaration`` () = @@ -656,6 +656,41 @@ let person = () " +[] +let ``multiline string before closing brace with anonymous record`` () = + 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 @@ -1041,6 +1076,27 @@ type Foo = member this.Foo() = () """ +[] +let ``record type definition with members and trivia`` () = + formatSourceString + false + """ +type X = { + Y: int +} with // foo + member x.Z = () +""" + { config with + NewlineBetweenTypeDefinitionAndMembers = false } + |> prepend newline + |> should + equal + """ +type X = + { Y: int } // foo + member x.Z = () +""" + [] let ``short anonymous record with two members`` () = formatSourceString @@ -1686,8 +1742,8 @@ let ``record with comments above field, indent 2`` () = equal """ { Foo = - // bar - someValue } + // bar + someValue } """ [] @@ -1743,8 +1799,8 @@ let ``anonymous record with multiline field, indent 2`` () = equal """ {| Foo = - // meh - someValue |} + // meh + someValue |} """ [] @@ -2124,30 +2180,6 @@ let compareThings (first: Thing) (second: Thing) = Bar = first.Bar } """ -[] -let ``equality comparison with a `with` expression should format correctly with Allman alignment, 2507`` () = - formatSourceString - false - """ -let compareThings (first: Thing) (second: Thing) = - first = { second with - Foo = first.Foo - Bar = first.Bar - } -""" - { config with - MultilineBracketStyle = Aligned } - |> prepend newline - |> should - equal - """ -let compareThings (first: Thing) (second: Thing) = - first = { second with - Foo = first.Foo - Bar = first.Bar - } -""" - [] let ``multiline record field type annotation`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/CursorTests.fs b/src/Fantomas.Core.Tests/CursorTests.fs new file mode 100644 index 0000000000..08d645e1e0 --- /dev/null +++ b/src/Fantomas.Core.Tests/CursorTests.fs @@ -0,0 +1,46 @@ +module Fantomas.Core.Tests.CursorTests + +open FSharp.Compiler.Text +open NUnit.Framework +open FsUnit +open Fantomas.Core + +let formatWithCursor source (line, column) = + CodeFormatter.FormatDocumentAsync(false, source, FormatConfig.Default, CodeFormatter.MakePosition(line, column)) + |> Async.RunSynchronously + +let assertCursor (expectedLine: int, expectedColumn: int) (result: FormatResult) : unit = + match result.Cursor with + | None -> Assert.Fail "Expected a cursor" + | Some cursor -> Assert.AreEqual(Position.mkPos expectedLine expectedColumn, cursor) + +[] +let ``cursor inside of a node`` () = + formatWithCursor + """ +let a = + "foobar" +""" + (3, 8) + |> assertCursor (1, 12) + +[] +let ``cursor outside of a node`` () = + formatWithCursor + """ +let a = + () +""" + (3, 7) + |> assertCursor (1, 11) + +[] +let ``cursor inside a node between defines`` () = + formatWithCursor + """ +#if FOO + () +#endif +""" + (3, 4) + |> assertCursor (2, 0) diff --git a/src/Fantomas.Core.Tests/DallasTests.fs b/src/Fantomas.Core.Tests/DallasTests.fs index ed5e27e7a7..2ecbbc8ddd 100644 --- a/src/Fantomas.Core.Tests/DallasTests.fs +++ b/src/Fantomas.Core.Tests/DallasTests.fs @@ -3,7 +3,7 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``proof of concept`` () = @@ -1857,7 +1857,7 @@ let someTest input1 input2 = } """ { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/DefinesTests.fs b/src/Fantomas.Core.Tests/DefinesTests.fs index 622f441c59..a79c97bba3 100644 --- a/src/Fantomas.Core.Tests/DefinesTests.fs +++ b/src/Fantomas.Core.Tests/DefinesTests.fs @@ -17,7 +17,7 @@ let private getDefines (v: string) = | ParsedInput.SigFile(ParsedSigFileInput(trivia = { ConditionalDirectives = directives })) -> directives getDefineCombination hashDirectives - |> List.collect id + |> List.collect (fun (DefineCombination(defines)) -> defines) |> List.distinct |> List.sort diff --git a/src/Fantomas.Core.Tests/DotGetTests.fs b/src/Fantomas.Core.Tests/DotGetTests.fs index 4b7545d2d5..227c2eb507 100644 --- a/src/Fantomas.Core.Tests/DotGetTests.fs +++ b/src/Fantomas.Core.Tests/DotGetTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.DotGetTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``a TypeApp inside a DotGet should stay on the same line, 994`` () = diff --git a/src/Fantomas.Core.Tests/DotIndexedGetTests.fs b/src/Fantomas.Core.Tests/DotIndexedGetTests.fs index d4a61345b6..67375ac7a2 100644 --- a/src/Fantomas.Core.Tests/DotIndexedGetTests.fs +++ b/src/Fantomas.Core.Tests/DotIndexedGetTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.DotIndexedGetTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``multiline function application inside DotIndexedGet`` () = diff --git a/src/Fantomas.Core.Tests/ExternTests.fs b/src/Fantomas.Core.Tests/ExternTests.fs index cb7f8d00d8..8d618d18fe 100644 --- a/src/Fantomas.Core.Tests/ExternTests.fs +++ b/src/Fantomas.Core.Tests/ExternTests.fs @@ -6,14 +6,14 @@ open Fantomas.Core.Tests.TestHelper [] let ``attribute above extern keyword, 562`` () = - formatSourceString + formatAST false """ module C = [] extern IntPtr f() """ - { config with StrictMode = true } + config |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index cf34cd5094..b0cb713268 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -21,7 +21,7 @@ - + @@ -35,6 +35,7 @@ + @@ -57,8 +58,8 @@ - - + + @@ -104,9 +105,11 @@ - + + + @@ -121,6 +124,8 @@ + + diff --git a/src/Fantomas.Core.Tests/FormattingSelectionOnlyTests.fs b/src/Fantomas.Core.Tests/FormattingSelectionOnlyTests.fs index 2144cd579d..17ec4e6cf0 100644 --- a/src/Fantomas.Core.Tests/FormattingSelectionOnlyTests.fs +++ b/src/Fantomas.Core.Tests/FormattingSelectionOnlyTests.fs @@ -6,7 +6,7 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -let private config = FormatConfig.FormatConfig.Default +let private config = FormatConfig.Default let private formatSelectionOnly isFsiFile selection (source: string) config = let formattedSelection, _ = diff --git a/src/Fantomas.Core.Tests/InterpolatedStringTests.fs b/src/Fantomas.Core.Tests/InterpolatedStringTests.fs index 468e4fcef2..e3b20c67b1 100644 --- a/src/Fantomas.Core.Tests/InterpolatedStringTests.fs +++ b/src/Fantomas.Core.Tests/InterpolatedStringTests.fs @@ -1,6 +1,5 @@ module Fantomas.Core.Tests.InterpolatedStringTests -open FSharp.Compiler.Text open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper @@ -64,13 +63,13 @@ let s = $\"\"\"%s{text} bar\"\"\" [] let ``interpolation in strict mode`` () = - formatSourceString + formatAST false """ let text = "foo" let s = $"%s{text} bar" """ - { config with StrictMode = true } + config |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/KeepIndentInBranchTests.fs b/src/Fantomas.Core.Tests/KeepIndentInBranchTests.fs index e16bbedd44..a1234ba95d 100644 --- a/src/Fantomas.Core.Tests/KeepIndentInBranchTests.fs +++ b/src/Fantomas.Core.Tests/KeepIndentInBranchTests.fs @@ -3,7 +3,7 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with diff --git a/src/Fantomas.Core.Tests/KeepMaxEmptyLinesTests.fs b/src/Fantomas.Core.Tests/KeepMaxEmptyLinesTests.fs index 7bbf472736..22aa8e2184 100644 --- a/src/Fantomas.Core.Tests/KeepMaxEmptyLinesTests.fs +++ b/src/Fantomas.Core.Tests/KeepMaxEmptyLinesTests.fs @@ -3,7 +3,7 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let checkFormat config source expected = formatSourceString false source config diff --git a/src/Fantomas.Core.Tests/LambdaTests.fs b/src/Fantomas.Core.Tests/LambdaTests.fs index 2b667a0ce7..96d2ee5ac2 100644 --- a/src/Fantomas.Core.Tests/LambdaTests.fs +++ b/src/Fantomas.Core.Tests/LambdaTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.LambdaTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``keep comment after arrow`` () = diff --git a/src/Fantomas.Core.Tests/LetBindingTests.fs b/src/Fantomas.Core.Tests/LetBindingTests.fs index eb2813c9b3..cf7262cabb 100644 --- a/src/Fantomas.Core.Tests/LetBindingTests.fs +++ b/src/Fantomas.Core.Tests/LetBindingTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.LetBindingTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``let in should be preserved`` () = diff --git a/src/Fantomas.Core.Tests/ListTests.fs b/src/Fantomas.Core.Tests/ListTests.fs index 7b7c4bb2b9..73d21d804b 100644 --- a/src/Fantomas.Core.Tests/ListTests.fs +++ b/src/Fantomas.Core.Tests/ListTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.ListTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``array indices`` () = diff --git a/src/Fantomas.Core.Tests/ModuleTests.fs b/src/Fantomas.Core.Tests/ModuleTests.fs index 5f2098ee4f..64217fcb98 100644 --- a/src/Fantomas.Core.Tests/ModuleTests.fs +++ b/src/Fantomas.Core.Tests/ModuleTests.fs @@ -393,7 +393,7 @@ type T() = CodeFormatter.FormatDocumentAsync(false, sourceCode, config) |> Async.RunSynchronously - |> fun s -> s.Replace("\r\n", "\n") + |> fun s -> s.Code.Replace("\r\n", "\n") |> should equal """open System diff --git a/src/Fantomas.Core.Tests/MultiLineLambdaClosingNewlineTests.fs b/src/Fantomas.Core.Tests/MultiLineLambdaClosingNewlineTests.fs index 99d23671d1..7e236f9f6d 100644 --- a/src/Fantomas.Core.Tests/MultiLineLambdaClosingNewlineTests.fs +++ b/src/Fantomas.Core.Tests/MultiLineLambdaClosingNewlineTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.MultiLineLambdaClosingNewlineTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let defaultConfig = config diff --git a/src/Fantomas.Core.Tests/MultipleDefineCombinationsTests.fs b/src/Fantomas.Core.Tests/MultipleDefineCombinationsTests.fs new file mode 100644 index 0000000000..13f8febc01 --- /dev/null +++ b/src/Fantomas.Core.Tests/MultipleDefineCombinationsTests.fs @@ -0,0 +1,194 @@ +module Fantomas.Core.Tests.MultipleDefineCombinationsTests + +open NUnit.Framework +open Fantomas.Core +open Fantomas.Core.Tests.TestHelper + +let private mergeAndCompare (aDefines, aCode) (bDefines, bCode) expected = + let result = + MultipleDefineCombinations.mergeMultipleFormatResults + { config with + EndOfLine = EndOfLineStyle.LF } + [ DefineCombination(aDefines), + { Code = String.normalizeNewLine aCode + Cursor = None } + DefineCombination(bDefines), + { Code = String.normalizeNewLine bCode + Cursor = None } ] + + let normalizedExpected = String.normalizeNewLine expected + normalizedExpected == result.Code + +[] +let ``merging of source code that starts with a hash`` () = + let a = + """#if NOT_DEFINED + printfn \"meh\" +#else + +#endif +""" + + let b = + """#if NOT_DEFINED + +#else + printfn \"foo\" +#endif +""" + + """#if NOT_DEFINED + printfn \"meh\" +#else + printfn \"foo\" +#endif +""" + |> mergeAndCompare ([], a) ([ "NOT_DEFINED" ], b) + +[] +let ``merging of defines content work when source code starts with a newline`` () = + let a = + """ +[] +let private assemblyConfig() = + #if TRACE + + #else + let x = "x" + #endif + x +""" + + let b = + """ +[] +let private assemblyConfig() = + #if TRACE + let x = "" + #else + + #endif + x +""" + + """ +[] +let private assemblyConfig() = +#if TRACE + let x = "" +#else + let x = "x" +#endif + x +""" + |> mergeAndCompare ([], a) ([ "TRACE" ], b) + +[] +let ``only split on control structure keyword`` () = + let a = + """ +#if INTERACTIVE +#else +#load "../FSharpx.TypeProviders/SetupTesting.fsx" + +SetupTesting.generateSetupScript __SOURCE_DIRECTORY__ + +#load "__setup__.fsx" +#endif +""" + + let b = + """ +#if INTERACTIVE +#else + + + +#endif + """ + + """ +#if INTERACTIVE +#else +#load "../FSharpx.TypeProviders/SetupTesting.fsx" + +SetupTesting.generateSetupScript __SOURCE_DIRECTORY__ + +#load "__setup__.fsx" +#endif +""" + |> mergeAndCompare ([], a) ([ "INTERACTIVE" ], b) + +// This test illustrates the goal of MultipleDefineCombinations +// All three results will be merged in one go. +[] +let ``triple merge`` () = + let result = + MultipleDefineCombinations.mergeMultipleFormatResults + { config with + EndOfLine = EndOfLineStyle.LF } + [ DefineCombination([]), + { Code = + String.normalizeNewLine + """ +let v = + #if A + + #else + #if B + + #else + 'C' + #endif + #endif +""" + Cursor = None } + DefineCombination([ "A" ]), + { Code = + String.normalizeNewLine + """ +let v = + #if A + 'A' + #else + #if B + + #else + + #endif + #endif +""" + Cursor = None } + DefineCombination([ "B" ]), + { Code = + String.normalizeNewLine + """ +let v = + #if A + + #else + #if B + 'B' + #else + + #endif + #endif +""" + Cursor = None } ] + + let expected = + String.normalizeNewLine + """ +let v = +#if A + 'A' +#else +#if B + 'B' +#else + 'C' +#endif +#endif +""" + + expected == result.Code diff --git a/src/Fantomas.Core.Tests/NewlineBeforeMultilineComputationExpressionTests.fs b/src/Fantomas.Core.Tests/NewlineBeforeMultilineComputationExpressionTests.fs new file mode 100644 index 0000000000..1d72f52339 --- /dev/null +++ b/src/Fantomas.Core.Tests/NewlineBeforeMultilineComputationExpressionTests.fs @@ -0,0 +1,724 @@ +module Fantomas.Core.Tests.NewlineBeforeMultilineComputationExpressionTests + +open NUnit.Framework +open FsUnit +open Fantomas.Core.Tests.TestHelper +open Fantomas.Core + +let config = + { config with + NewlineBeforeMultilineComputationExpression = false + MaxArrayOrListWidth = 40 } + +[] +let ``prefer computation expression name on same line`` () = + formatSourceString + false + """ +let t = + task { + let! thing = otherThing () + return 5 + } +""" + config + |> prepend newline + |> should + equal + """ +let t = task { + let! thing = otherThing () + return 5 +} +""" + +[] +let ``prefer computation expression name on same line handling short expression`` () = + formatSourceString + false + """ +let t = + task { + return () + } +""" + config + |> prepend newline + |> should + equal + """ +let t = task { return () } +""" + +[] +let ``application parenthesis expr dotIndexedSet with computation expression`` () = + formatSourceString + false + """ +app(meh).[x] <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +app( + meh +).[x] <- task { + // some computation here + () +} +""" + +[] +let ``application unit dotIndexedSet with computation expression`` () = + formatSourceString + false + """ +app().[x] <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +app().[x] <- task { + // some computation here + () +} +""" + +[] +let ``dotIndexedSet with computation expression`` () = + formatSourceString + false + """ +myMutable.[x] <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +myMutable.[x] <- task { + // some computation here + () +} +""" + +[] +let ``dotSet with computation expression`` () = + formatSourceString + false + """ +App().foo <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +App().foo <- task { + // some computation here + () +} +""" + +[] +let ``app paren lambda with computation expression`` () = + formatSourceString + false + """ +List.map (fun x -> + task { + // some computation here + () + }) +""" + config + |> prepend newline + |> should + equal + """ +List.map (fun x -> task { + // some computation here + () +}) +""" + +[] +let ``app paren lambda with computation expression and other args`` () = + formatSourceString + false + """ +List.map (fun x -> + task { + // some computation here + () + }) b c +""" + config + |> prepend newline + |> should + equal + """ +List.map + (fun x -> task { + // some computation here + () + }) + b + c +""" + +[] +let ``dotGetApp with lambda with computation expression`` () = + formatSourceString + false + """ +Bar + .Foo(fun x -> + task { + // some computation here + () + }).Bar() +""" + config + |> prepend newline + |> should + equal + """ +Bar + .Foo(fun x -> task { + // some computation here + () + }) + .Bar() +""" + +[] +let ``lambda with computation expression`` () = + formatSourceString + false + """ +fun x -> + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +fun x -> task { + // some computation here + () +} +""" + +[] +let ``letOrUseBang with computation expression`` () = + formatSourceString + false + """ +task { + let! meh = + task { + // comment + return 42 + } + () +} +""" + config + |> prepend newline + |> should + equal + """ +task { + let! meh = task { + // comment + return 42 + } + + () +} +""" + +[] +let ``longIdentSet with computation expression`` () = + formatSourceString + false + """ +myMutable <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +myMutable <- task { + // some computation here + () +} +""" + +[] +let ``paren lambda with computation expression`` () = + formatSourceString + false + """ +(fun x -> + task { + // some computation here + () + }) +""" + config + |> prepend newline + |> should + equal + """ +(fun x -> task { + // some computation here + () +}) +""" + +[] +let ``synExprApp with named argument with computation expression`` () = + formatSourceString + false + """ +let v = + SomeConstructor( + v = + task { + // some computation here + () + } + ) +""" + config + |> prepend newline + |> should + equal + """ +let v = + SomeConstructor( + v = task { + // some computation here + () + } + ) +""" + +[] +let ``synExprNew with named argument with computation expression`` () = + formatSourceString + false + """ +let v = + new FooBar( + v = + task { + // some computation here + () + } + ) +""" + config + |> prepend newline + |> should + equal + """ +let v = + new FooBar( + v = task { + // some computation here + () + } + ) +""" + +[] +let ``set with computation expression`` () = + formatSourceString + false + """ +myMutable[x] <- + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +myMutable[x] <- task { + // some computation here + () +} +""" + +[] +let ``synbinding function with computation expression`` () = + formatSourceString + false + """ +let x y = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +let x y = task { + // some computation here + () +} +""" + +[] +let ``synbinding function with computation expression with return type`` () = + formatSourceString + false + """ +let x y: Task = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +let x y : Task = task { + // some computation here + () +} +""" + +[] +let ``type member function with computation expression`` () = + formatSourceString + false + """ +type Foo() = + member this.Bar x = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +type Foo() = + member this.Bar x = task { + // some computation here + () + } +""" + +[] +let ``type member function with computation expression with return type`` () = + formatSourceString + false + """ +type Foo() = + member this.Bar x : Task = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +type Foo() = + member this.Bar x : Task = task { + // some computation here + () + } +""" + +[] +let ``synbinding value with computation expression`` () = + formatSourceString + false + """ +let t = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +let t = task { + // some computation here + () +} +""" + +[] +let ``type member value with computation expression`` () = + formatSourceString + false + """ +type Foo() = + member this.Bar = + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +type Foo() = + member this.Bar = task { + // some computation here + () + } +""" + +[] +let ``andBang with computation expression`` () = + formatSourceString + false + """ +task { + let! abc = def () + and! meh = + task { + // comment + return 42 + } + () +} +""" + config + |> prepend newline + |> should + equal + """ +task { + let! abc = def () + + and! meh = task { + // comment + return 42 + } + + () +} +""" + +[] +let ``synMatchClause in match expression with computation expression`` () = + formatSourceString + false + """ +match x with +| _ -> + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +match x with +| _ -> task { + // some computation here + () + } +""" + +[] +let ``synMatchClause in try/with expression with computation expression`` () = + formatSourceString + false + """ +try + foo() +with +| ex -> + task { + // some computation here + () + } +""" + config + |> prepend newline + |> should + equal + """ +try + foo () +with ex -> task { + // some computation here + () +} +""" + +[] +let ``yieldOrReturnBang with computation expression`` () = + formatSourceString + false + """ +myComp { + yield! + seq { + // meh + return 0 .. 2 + } + return! + seq { + // meh + return 0 .. 2 + } +} +""" + config + |> prepend newline + |> should + equal + """ +myComp { + yield! seq { + // meh + return 0..2 + } + + return! seq { + // meh + return 0..2 + } +} +""" + +[] +let ``yieldOrReturn with computation expression`` () = + formatSourceString + false + """ +myComp { + yield + seq { + // meh + return 0 .. 2 + } + return + seq { + // meh + return 0 .. 2 + } +} +""" + config + |> prepend newline + |> should + equal + """ +myComp { + yield seq { + // meh + return 0..2 + } + + return seq { + // meh + return 0..2 + } +} +""" + +[] +let ``prefer computation expression name on same line, with trivia`` () = + formatSourceString + false + """ +let t = + // + task { + let! thing = otherThing () + return 5 + } +""" + config + |> prepend newline + |> should + equal + """ +let t = + // + task { + let! thing = otherThing () + return 5 + } +""" + +[] +let ``fsharp_multiline_bracket_style = stroustrup has not influence`` () = + formatSourceString + false + """ +fun _ -> task { // foo + () } +""" + { FormatConfig.Default with + MultilineBracketStyle = Stroustrup } + |> prepend newline + |> should + equal + """ +fun _ -> + task { // foo + () + } +""" diff --git a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs index 628abb822e..a0d3fefdda 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.NumberOfItemsListOrArrayTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``number of items sized lists are formatted properly`` () = @@ -88,7 +88,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -206,7 +206,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -240,7 +240,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -272,7 +272,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs index b882e1f54c..c03f892f58 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.NumberOfItemsRecordTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with @@ -697,8 +697,8 @@ let ``indent update anonymous record fields far enough`` () = """ let expected = {| ThisIsAThing.Empty with - TheNewValue = 1 - ThatValue = 2 |} + TheNewValue = 1 + ThatValue = 2 |} """ [] diff --git a/src/Fantomas.Core.Tests/OpenTypeTests.fs b/src/Fantomas.Core.Tests/OpenTypeTests.fs index c530c0f6fa..073d08fab4 100644 --- a/src/Fantomas.Core.Tests/OpenTypeTests.fs +++ b/src/Fantomas.Core.Tests/OpenTypeTests.fs @@ -1,6 +1,5 @@ module Fantomas.Core.Tests.OpenTypeTests -open Fantomas open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper diff --git a/src/Fantomas.Core.Tests/OperatorTests.fs b/src/Fantomas.Core.Tests/OperatorTests.fs index a22ab7011d..c2614a41c6 100644 --- a/src/Fantomas.Core.Tests/OperatorTests.fs +++ b/src/Fantomas.Core.Tests/OperatorTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.OperatorTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``should format prefix operators`` () = diff --git a/src/Fantomas.Core.Tests/SignatureTests.fs b/src/Fantomas.Core.Tests/SignatureTests.fs index fa66d252ba..e182ba1829 100644 --- a/src/Fantomas.Core.Tests/SignatureTests.fs +++ b/src/Fantomas.Core.Tests/SignatureTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.SignatureTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core // the current behavior results in a compile error since "(string * string) list" is converted to "string * string list" [] diff --git a/src/Fantomas.Core.Tests/SpaceBeforeClassConstructorTests.fs b/src/Fantomas.Core.Tests/SpaceBeforeClassConstructorTests.fs index 1c744fec13..0b6e608be1 100644 --- a/src/Fantomas.Core.Tests/SpaceBeforeClassConstructorTests.fs +++ b/src/Fantomas.Core.Tests/SpaceBeforeClassConstructorTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.SpaceBeforeClassConstructorTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let spaceBeforeConfig = { config with diff --git a/src/Fantomas.Core.Tests/StringTests.fs b/src/Fantomas.Core.Tests/StringTests.fs index 38733e2569..f3297036fb 100644 --- a/src/Fantomas.Core.Tests/StringTests.fs +++ b/src/Fantomas.Core.Tests/StringTests.fs @@ -113,7 +113,7 @@ let g = '\n' [] let ``uncommon literals strict mode`` () = - formatSourceString + formatAST false """ let a = 0xFFy @@ -123,7 +123,7 @@ let e = 1.40e10f let f = 23.4M let g = '\n' """ - { config with StrictMode = true } + config |> prepend newline |> should equal @@ -219,14 +219,14 @@ let ``chars should be properly escaped`` () = [] let ``quotes should be escaped in strict mode`` () = - formatSourceString + formatAST false """ let formatter = // escape commas left in invalid entries sprintf "%i,\"%s\"" """ - { config with StrictMode = true } + config |> should equal """let formatter = sprintf "%i,\"%s\"" diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs index 88d69a4e49..8f881af18e 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ myMutable.[x] <- |> should equal """ -myMutable.[x] <- - { astContext with +myMutable.[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -96,28 +96,6 @@ myMutable.[x] <- struct {| |} """ -[] -let ``dotIndexedSet with computation expression`` () = - formatSourceString - false - """ -myMutable.[x] <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -myMutable.[x] <- task { - // some computation here - () -} -""" - [] let ``dotIndexedSet with list`` () = formatSourceString @@ -205,10 +183,10 @@ app().[x] <- |> should equal """ -app().[x] <- - { astContext with +app().[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -256,28 +234,6 @@ app().[x] <- struct {| |} """ -[] -let ``application unit dotIndexedSet with computation expression`` () = - formatSourceString - false - """ -app().[x] <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -app().[x] <- task { - // some computation here - () -} -""" - [] let ``application unit dotIndexedSet with list`` () = formatSourceString @@ -371,10 +327,10 @@ app(meh).[x] <- """ app( meh -).[x] <- - { astContext with +).[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -426,30 +382,6 @@ app( |} """ -[] -let ``application parenthesis expr dotIndexedSet with computation expression`` () = - formatSourceString - false - """ -app(meh).[x] <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -app( - meh -).[x] <- task { - // some computation here - () -} -""" - [] let ``application parenthesis expr dotIndexedSet with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs index e345eb9a6e..db191b34db 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -68,10 +68,10 @@ App().foo <- |> should equal """ -App().foo <- - { astContext with +App().foo <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -119,28 +119,6 @@ App().foo <- struct {| |} """ -[] -let ``dotSet with computation expression`` () = - formatSourceString - false - """ -App().foo <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -App().foo <- task { - // some computation here - () -} -""" - [] let ``dotSet with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs b/src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs similarity index 84% rename from src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs rename to src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs index 22d60553c3..5e7d871f49 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs @@ -1,158 +1,13 @@ -module Fantomas.Core.Tests.ElmishTests +module Fantomas.Core.Tests.Stroustrup.ExperimentalElmishTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } - -[] -let ``long named arguments should go on newline`` () = - formatSourceString - false - """let view (model: Model) dispatch = - View.ContentPage( - appearing=(fun () -> dispatch PageAppearing), - title=model.Planet.Info.Name, - backgroundColor=Color.Black, - content=["....long line....................................................................................................."] - ) -""" - config - |> prepend newline - |> should - equal - """ -let view (model: Model) dispatch = - View.ContentPage( - appearing = (fun () -> dispatch PageAppearing), - title = model.Planet.Info.Name, - backgroundColor = Color.Black, - content = [ - "....long line....................................................................................................." - ] - ) -""" - -[] -let ``single view entry`` () = - formatSourceString - false - """ -let a = - View.Entry( - placeholder = "User name", - isEnabled = (not model.IsSigningIn), - textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue)))) -""" - config - |> prepend newline - |> should - equal - """ -let a = - View.Entry( - placeholder = "User name", - isEnabled = (not model.IsSigningIn), - textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue))) - ) -""" - -[] -let ``fabulous view`` () = - formatSourceString - false - """ - let loginPage = - View.ContentPage( - title = "Fabulous Demo", - content = View.ScrollView( - content = View.StackLayout( - padding = 30.0, - children = [ - View.Frame( - verticalOptions = LayoutOptions.CenterAndExpand, - content = View.StackLayout(children = [ - View.Entry( - placeholder = "User name", - isEnabled = (not model.IsSigningIn), - textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue)))) - View.Entry( - placeholder = "Password", - isPassword = true, - isEnabled = (not model.IsSigningIn), - textChanged = (fun args -> (dispatch (PasswordChanged args.NewTextValue)))) - View.Button( - text = "Sign in", - heightRequest = 30.0, - isVisible = (not model.IsSigningIn), - command = (fun () -> dispatch SignIn), - canExecute = model.IsCredentialsProvided) - View.ActivityIndicator( - isRunning = true, - heightRequest = 30.0, - isVisible = model.IsSigningIn)]) - ) - ] - ) - ) - ) -""" - config - |> prepend newline - |> should - equal - """ -let loginPage = - View.ContentPage( - title = "Fabulous Demo", - content = - View.ScrollView( - content = - View.StackLayout( - padding = 30.0, - children = [ - View.Frame( - verticalOptions = LayoutOptions.CenterAndExpand, - content = - View.StackLayout( - children = [ - View.Entry( - placeholder = "User name", - isEnabled = (not model.IsSigningIn), - textChanged = - (fun args -> (dispatch (UserNameChanged args.NewTextValue))) - ) - View.Entry( - placeholder = "Password", - isPassword = true, - isEnabled = (not model.IsSigningIn), - textChanged = - (fun args -> (dispatch (PasswordChanged args.NewTextValue))) - ) - View.Button( - text = "Sign in", - heightRequest = 30.0, - isVisible = (not model.IsSigningIn), - command = (fun () -> dispatch SignIn), - canExecute = model.IsCredentialsProvided - ) - View.ActivityIndicator( - isRunning = true, - heightRequest = 30.0, - isVisible = model.IsSigningIn - ) - ] - ) - ) - ] - ) - ) - ) -""" + ExperimentalElmish = true } [] let ``input without attributes`` () = @@ -1048,10 +903,8 @@ let private useLocationDetail (auth0 : Auth0Hook) (roles : RolesHook) id = match usersResult with | Ok name -> setCreatorName (Some name) | Error err -> JS.console.log err)), - [| - box roles.Roles - box location.Creator - |] + [| box roles.Roles + box location.Creator |] ) location, creatorName @@ -1355,7 +1208,7 @@ let Dashboard () = ] """ { config with - RecordMultilineFormatter = Fantomas.Core.FormatConfig.MultilineFormatterType.NumberOfItems + RecordMultilineFormatter = MultilineFormatterType.NumberOfItems MaxArrayOrListWidth = 20 // MaxElmishWidth = 10 MultiLineLambdaClosingNewline = true } @@ -1528,3 +1381,137 @@ ReactDom.render ( root ) """ + +[] +let ``record type definition and elmish dsl are controlled separately`` () = + formatSourceString + false + """ +type Point = + { + /// Great comment + X: int + Y: int + } + +type Model = { + Points: Point list +} + +let view dispatch model = + div + [] + [ + h1 [] [ str "Some title" ] + ul + [] + [ + for p in model.Points do + li [] [ str $"%i{p.X}, %i{p.Y}" ] + ] + hr [] + ] + +let stillCramped = [ + // yow + x ; y ; z +] +""" + config + |> prepend newline + |> should + equal + """ +type Point = + { + /// Great comment + X: int + Y: int + } + +type Model = { Points: Point list } + +let view dispatch model = + div [] [ + h1 [] [ str "Some title" ] + ul [] [ + for p in model.Points do + li [] [ str $"%i{p.X}, %i{p.Y}" ] + ] + hr [] + ] + +let stillCramped = + [ + // yow + x + y + z ] +""" + +[] +let ``fsharp_multiline_bracket_style = stroustrup also applies for applications that ends with list arguments`` () = + formatSourceString + false + """ +type Point = + { + /// Great comment + X: int + Y: int + } + +type Model = { + Points: Point list +} + +let view dispatch model = + div + [] + [ + h1 [] [ str "Some title" ] + ul + [] + [ + for p in model.Points do + li [] [ str $"%i{p.X}, %i{p.Y}" ] + ] + hr [] + ] + +let alsoStroup = [ + // yow + x ; y ; z +] +""" + { FormatConfig.Default with + MultilineBracketStyle = Stroustrup } + |> prepend newline + |> should + equal + """ +type Point = { + /// Great comment + X: int + Y: int +} + +type Model = { Points: Point list } + +let view dispatch model = + div [] [ + h1 [] [ str "Some title" ] + ul [] [ + for p in model.Points do + li [] [ str $"%i{p.X}, %i{p.Y}" ] + ] + hr [] + ] + +let alsoStroup = [ + // yow + x + y + z +] +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs index 5bc637a864..86e6578a99 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } [] let ``two short lists`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs index a100c73de5..9b54989810 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + ExperimentalElmish = true } [] let ``short function application`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs index 986252a46e..55054b5833 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs @@ -3,14 +3,14 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core // ExperimentalKeepIndentInBranch has precedence over ExperimentalStroustrupStyle let config = { config with ExperimentalKeepIndentInBranch = true - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } // There currently is no conflict with this setting, but I'm guessing the case was never brought up. @@ -57,8 +57,9 @@ match x with """ match x with | _ -> - { astContext with - IsInsideMatchClausePattern = true + { + astContext with + IsInsideMatchClausePattern = true } """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs index 74b944bd77..d1c80fd6b3 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ fun x -> |> should equal """ -fun x -> - { astContext with +fun x -> { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -96,28 +96,6 @@ fun x -> struct {| |} """ -[] -let ``lambda with computation expression`` () = - formatSourceString - false - """ -fun x -> - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -fun x -> task { - // some computation here - () -} -""" - [] let ``lambda with list`` () = formatSourceString @@ -205,10 +183,10 @@ let ``paren lambda with update record`` () = |> should equal """ -(fun x -> - { astContext with +(fun x -> { + astContext with IsInsideMatchClausePattern = true - }) +}) """ [] @@ -256,28 +234,6 @@ let ``paren lambda with anonymous record instance struct`` () = |}) """ -[] -let ``paren lambda with computation expression`` () = - formatSourceString - false - """ -(fun x -> - task { - // some computation here - () - }) -""" - config - |> prepend newline - |> should - equal - """ -(fun x -> task { - // some computation here - () -}) -""" - [] let ``paren lambda with list`` () = formatSourceString @@ -365,10 +321,10 @@ List.map (fun x -> |> should equal """ -List.map (fun x -> - { astContext with +List.map (fun x -> { + astContext with IsInsideMatchClausePattern = true - }) +}) """ [] @@ -416,28 +372,6 @@ List.map (fun x -> struct {| |}) """ -[] -let ``app paren lambda with computation expression`` () = - formatSourceString - false - """ -List.map (fun x -> - task { - // some computation here - () - }) -""" - config - |> prepend newline - |> should - equal - """ -List.map (fun x -> task { - // some computation here - () -}) -""" - [] let ``app paren lambda with list`` () = formatSourceString @@ -529,10 +463,10 @@ List.map (fun x -> equal """ List.map - (fun x -> - { astContext with + (fun x -> { + astContext with IsInsideMatchClausePattern = true - }) + }) b c """ @@ -588,31 +522,6 @@ List.map c """ -[] -let ``app paren lambda with computation expression and other args`` () = - formatSourceString - false - """ -List.map (fun x -> - task { - // some computation here - () - }) b c -""" - config - |> prepend newline - |> should - equal - """ -List.map - (fun x -> task { - // some computation here - () - }) - b - c -""" - [] let ``app paren lambda with list and other args`` () = formatSourceString @@ -713,13 +622,13 @@ Bar.Foo(fun x -> { other with equal """ Bar - .Foo(fun x -> - { other with + .Foo(fun x -> { + other with A = longTypeName B = someOtherVariable C = ziggyBarX D = evenMoreZigBarry - }) + }) .Bar() """ @@ -773,31 +682,6 @@ Bar .Bar() """ -[] -let ``dotGetApp with lambda with computation expression`` () = - formatSourceString - false - """ -Bar - .Foo(fun x -> - task { - // some computation here - () - }).Bar() -""" - config - |> prepend newline - |> should - equal - """ -Bar - .Foo(fun x -> task { - // some computation here - () - }) - .Bar() -""" - [] let ``dotGetApp with lambda with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs index e04c7b04ac..6c2eee6a1b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -60,12 +60,12 @@ opt { equal """ opt { - let! foo = - { bar with + let! foo = { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } () } @@ -131,35 +131,6 @@ opt { } """ -[] -let ``letOrUseBang with computation expression`` () = - formatSourceString - false - """ -task { - let! meh = - task { - // comment - return 42 - } - () -} -""" - config - |> prepend newline - |> should - equal - """ -task { - let! meh = task { - // comment - return 42 - } - - () -} -""" - [] let ``letOrUseBang with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs index f230f6db86..f4b7386886 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ myMutable <- |> should equal """ -myMutable <- - { astContext with +myMutable <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -96,28 +96,6 @@ myMutable <- struct {| |} """ -[] -let ``longIdentSet with computation expression`` () = - formatSourceString - false - """ -myMutable <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -myMutable <- task { - // some computation here - () -} -""" - [] let ``longIdentSet with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs index 8540d0ca21..6cd4f24c8d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs @@ -3,12 +3,12 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with MultiLineLambdaClosingNewline = true - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -46,11 +46,10 @@ let ``paren lambda with update record`` () = |> should equal """ -(fun x -> - { astContext with +(fun x -> { + astContext with IsInsideMatchClausePattern = true - } -) +}) """ [] @@ -98,28 +97,6 @@ let ``paren lambda with anonymous record instance struct`` () = |}) """ -[] -let ``paren lambda with computation expression`` () = - formatSourceString - false - """ -(fun x -> - task { - // some computation here - () - }) -""" - config - |> prepend newline - |> should - equal - """ -(fun x -> task { - // some computation here - () -}) -""" - [] let ``paren lambda with list`` () = formatSourceString @@ -207,11 +184,10 @@ List.map (fun x -> |> should equal """ -List.map (fun x -> - { astContext with +List.map (fun x -> { + astContext with IsInsideMatchClausePattern = true - } -) +}) """ [] @@ -259,28 +235,6 @@ List.map (fun x -> struct {| |}) """ -[] -let ``app paren lambda with computation expression`` () = - formatSourceString - false - """ -List.map (fun x -> - task { - // some computation here - () - }) -""" - config - |> prepend newline - |> should - equal - """ -List.map (fun x -> task { - // some computation here - () -}) -""" - [] let ``app paren lambda with list`` () = formatSourceString @@ -372,11 +326,10 @@ List.map (fun x -> equal """ List.map - (fun x -> - { astContext with + (fun x -> { + astContext with IsInsideMatchClausePattern = true - } - ) + }) b c """ @@ -432,31 +385,6 @@ List.map c """ -[] -let ``app paren lambda with computation expression and other args`` () = - formatSourceString - false - """ -List.map (fun x -> - task { - // some computation here - () - }) b c -""" - config - |> prepend newline - |> should - equal - """ -List.map - (fun x -> task { - // some computation here - () - }) - b - c -""" - [] let ``app paren lambda with list and other args`` () = formatSourceString @@ -557,14 +485,13 @@ Bar.Foo(fun x -> { other with equal """ Bar - .Foo(fun x -> - { other with + .Foo(fun x -> { + other with A = longTypeName B = someOtherVariable C = ziggyBarX D = evenMoreZigBarry - } - ) + }) .Bar() """ @@ -618,31 +545,6 @@ Bar .Bar() """ -[] -let ``dotGetApp with lambda with computation expression`` () = - formatSourceString - false - """ -Bar - .Foo(fun x -> - task { - // some computation here - () - }).Bar() -""" - config - |> prepend newline - |> should - equal - """ -Bar - .Foo(fun x -> task { - // some computation here - () - }) - .Bar() -""" - [] let ``dotGetApp with lambda with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs index 716a642543..e071d88ba6 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -59,13 +59,13 @@ let v = """ let v = SomeConstructor( - v = - { astContext with + v = { + astContext with IsInsideMatchClausePattern = true A = longTypeName B = someOtherVariable C = ziggyBarX - } + } ) """ @@ -125,34 +125,6 @@ let v = ) """ -[] -let ``synExprApp with named argument with computation expression`` () = - formatSourceString - false - """ -let v = - SomeConstructor( - v = - task { - // some computation here - () - } - ) -""" - config - |> prepend newline - |> should - equal - """ -let v = - SomeConstructor( - v = task { - // some computation here - () - } - ) -""" - [] let ``synExprApp with named argument with list`` () = formatSourceString @@ -313,13 +285,13 @@ let v = """ let v = new FooBar( - v = - { astContext with + v = { + astContext with IsInsideMatchClausePattern = true A = longTypeName B = someOtherVariable C = ziggyBarX - } + } ) """ @@ -379,34 +351,6 @@ let v = ) """ -[] -let ``synExprNew with named argument with computation expression`` () = - formatSourceString - false - """ -let v = - new FooBar( - v = - task { - // some computation here - () - } - ) -""" - config - |> prepend newline - |> should - equal - """ -let v = - new FooBar( - v = task { - // some computation here - () - } - ) -""" - [] let ``synExprNew with named argument with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/NamedParameterTests.fs b/src/Fantomas.Core.Tests/Stroustrup/NamedParameterTests.fs new file mode 100644 index 0000000000..562621ac2b --- /dev/null +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedParameterTests.fs @@ -0,0 +1,155 @@ +module Fantomas.Core.Tests.NamedParameterTests + +open NUnit.Framework +open FsUnit +open Fantomas.Core.Tests.TestHelper +open Fantomas.Core + +let config = + { config with + MultilineBracketStyle = Stroustrup } + +[] +let ``long named arguments should go on newline`` () = + formatSourceString + false + """let view (model: Model) dispatch = + View.ContentPage( + appearing=(fun () -> dispatch PageAppearing), + title=model.Planet.Info.Name, + backgroundColor=Color.Black, + content=["....long line....................................................................................................."] + ) +""" + config + |> prepend newline + |> should + equal + """ +let view (model: Model) dispatch = + View.ContentPage( + appearing = (fun () -> dispatch PageAppearing), + title = model.Planet.Info.Name, + backgroundColor = Color.Black, + content = [ + "....long line....................................................................................................." + ] + ) +""" + +[] +let ``single view entry`` () = + formatSourceString + false + """ +let a = + View.Entry( + placeholder = "User name", + isEnabled = (not model.IsSigningIn), + textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue)))) +""" + config + |> prepend newline + |> should + equal + """ +let a = + View.Entry( + placeholder = "User name", + isEnabled = (not model.IsSigningIn), + textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue))) + ) +""" + +[] +let ``fabulous view`` () = + formatSourceString + false + """ + let loginPage = + View.ContentPage( + title = "Fabulous Demo", + content = View.ScrollView( + content = View.StackLayout( + padding = 30.0, + children = [ + View.Frame( + verticalOptions = LayoutOptions.CenterAndExpand, + content = View.StackLayout(children = [ + View.Entry( + placeholder = "User name", + isEnabled = (not model.IsSigningIn), + textChanged = (fun args -> (dispatch (UserNameChanged args.NewTextValue)))) + View.Entry( + placeholder = "Password", + isPassword = true, + isEnabled = (not model.IsSigningIn), + textChanged = (fun args -> (dispatch (PasswordChanged args.NewTextValue)))) + View.Button( + text = "Sign in", + heightRequest = 30.0, + isVisible = (not model.IsSigningIn), + command = (fun () -> dispatch SignIn), + canExecute = model.IsCredentialsProvided) + View.ActivityIndicator( + isRunning = true, + heightRequest = 30.0, + isVisible = model.IsSigningIn)]) + ) + ] + ) + ) + ) +""" + config + |> prepend newline + |> should + equal + """ +let loginPage = + View.ContentPage( + title = "Fabulous Demo", + content = + View.ScrollView( + content = + View.StackLayout( + padding = 30.0, + children = [ + View.Frame( + verticalOptions = LayoutOptions.CenterAndExpand, + content = + View.StackLayout( + children = [ + View.Entry( + placeholder = "User name", + isEnabled = (not model.IsSigningIn), + textChanged = + (fun args -> (dispatch (UserNameChanged args.NewTextValue))) + ) + View.Entry( + placeholder = "Password", + isPassword = true, + isEnabled = (not model.IsSigningIn), + textChanged = + (fun args -> (dispatch (PasswordChanged args.NewTextValue))) + ) + View.Button( + text = "Sign in", + heightRequest = 30.0, + isVisible = (not model.IsSigningIn), + command = (fun () -> dispatch SignIn), + canExecute = model.IsCredentialsProvided + ) + View.ActivityIndicator( + isRunning = true, + heightRequest = 30.0, + isVisible = model.IsSigningIn + ) + ] + ) + ) + ] + ) + ) + ) +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs index 6ad063f916..bfc83edf8d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ myMutable[x] <- |> should equal """ -myMutable[x] <- - { astContext with +myMutable[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -96,28 +96,6 @@ myMutable[x] <- struct {| |} """ -[] -let ``set with computation expression`` () = - formatSourceString - false - """ -myMutable[x] <- - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -myMutable[x] <- task { - // some computation here - () -} -""" - [] let ``set with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs index f48d866057..219ec5e8d6 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ let x y = |> should equal """ -let x y = - { astContext with +let x y = { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -73,28 +73,6 @@ let x y = {| |} """ -[] -let ``synbinding function with computation expression`` () = - formatSourceString - false - """ -let x y = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -let x y = task { - // some computation here - () -} -""" - [] let ``synbinding function with list`` () = formatSourceString @@ -185,10 +163,10 @@ type Foo() = equal """ type Foo() = - member this.Bar x = - { astContext with + member this.Bar x = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -240,30 +218,6 @@ type Foo() = |} """ -[] -let ``type member function with computation expression`` () = - formatSourceString - false - """ -type Foo() = - member this.Bar x = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -type Foo() = - member this.Bar x = task { - // some computation here - () - } -""" - [] let ``type member function with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs index b47c444e4a..3c990d3c51 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs @@ -2,13 +2,13 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with MaxLineLength = 80 - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } // TODO: conclude on what should happen here @@ -78,8 +78,9 @@ let private addTaskToScheduler (task: unit -> unit) groupName = - { astContext with - IsInsideMatchClausePattern = true + { + astContext with + IsInsideMatchClausePattern = true } """ @@ -307,8 +308,9 @@ type Foo() = (task: unit -> unit) groupName = - { astContext with - IsInsideMatchClausePattern = true + { + astContext with + IsInsideMatchClausePattern = true } """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs index 9b2f224d53..19ff09f198 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -45,10 +45,10 @@ let x y : MyRecord = |> should equal """ -let x y : MyRecord = - { astContext with +let x y : MyRecord = { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -73,28 +73,6 @@ let x y : {| A: int; B: int; C: int |} = {| |} """ -[] -let ``synbinding function with computation expression`` () = - formatSourceString - false - """ -let x y: Task = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -let x y : Task = task { - // some computation here - () -} -""" - [] let ``synbinding function with list`` () = formatSourceString @@ -185,10 +163,10 @@ type Foo() = equal """ type Foo() = - member this.Bar x : MyRecord = - { astContext with + member this.Bar x : MyRecord = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -240,30 +218,6 @@ type Foo() = |} """ -[] -let ``type member function with computation expression`` () = - formatSourceString - false - """ -type Foo() = - member this.Bar x : Task = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -type Foo() = - member this.Bar x : Task = task { - // some computation here - () - } -""" - [] let ``type member function with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs index 84e4040459..0ca525fdaa 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -38,17 +38,40 @@ let ``synbinding value with update record`` () = false """ let astCtx = - { astContext with IsInsideMatchClausePattern = true } + { astContext with IsInsideMatchClausePattern = true; OtherThing = "YOLO" } """ - config + { config with + RecordMultilineFormatter = NumberOfItems } |> prepend newline |> should equal """ +let astCtx = { + astContext with + IsInsideMatchClausePattern = true + OtherThing = "YOLO" +} +""" + +[] +let ``synbinding value with update anonymous record`` () = + formatSourceString + false + """ let astCtx = - { astContext with + {| astContext with IsInsideMatchClausePattern = true; OtherThing = "YOLO" |} +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let astCtx = {| + astContext with IsInsideMatchClausePattern = true - } + OtherThing = "YOLO" +|} """ [] @@ -96,28 +119,6 @@ let x = struct {| |} """ -[] -let ``synbinding value with computation expression`` () = - formatSourceString - false - """ -let t = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -let t = task { - // some computation here - () -} -""" - [] let ``synbinding value with list`` () = formatSourceString @@ -235,10 +236,10 @@ type Foo() = equal """ type Foo() = - member this.Bar = - { astContext with + member this.Bar = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -290,30 +291,6 @@ type Foo() = |} """ -[] -let ``type member value with computation expression`` () = - formatSourceString - false - """ -type Foo() = - member this.Bar = - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -type Foo() = - member this.Bar = task { - // some computation here - () - } -""" - [] let ``type member value with list`` () = formatSourceString @@ -371,7 +348,7 @@ type Foo() = """ [] -let ``let binding for anonymous record with expression, 2508`` () = +let ``let binding for anonymous record with copy expression, 2508`` () = formatSourceString false """ @@ -389,14 +366,14 @@ let fooDto = |> should equal """ -let fooDto = - {| otherDto with +let fooDto = {| + otherDto with TextFilters = criteria.Meta.TextFilter |> Option.map (fun f -> f.Filters) |> Option.map (List.map (sprintf "~%s~")) |> Option.toObj - |} +|} """ [] @@ -592,3 +569,153 @@ let myRecord = { } } """ + +[] +let ``app node with single record member`` () = + formatSourceString + false + """ +let newState = { + Foo = + Some + { + F1 = 0 + F2 = "" + } +} +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = { + Foo = + Some { + F1 = 0 + F2 = "" + } +} +""" + +[] +let ``app node with single anonymous record member`` () = + formatSourceString + false + """ +let newState = {| + Foo = + Some + {| + F1 = 0 + F2 = "" + |} +|} +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = {| + Foo = + Some {| + F1 = 0 + F2 = "" + |} +|} +""" + +[] +let ``app node with single record arg`` () = + formatSourceString + false + """ +let newState = + Some + { + F1 = 0 + F2 = "" + } +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = + Some { + F1 = 0 + F2 = "" + } +""" + +[] +let ``lowercase app node with single record arg`` () = + formatSourceString + false + """ +let newState = + someFunc + { + F1 = 0 + F2 = "" + } +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = + someFunc { + F1 = 0 + F2 = "" + } +""" + +[] +let ``lowercase app node with multiple args ending in a single record arg`` () = + formatSourceString + false + """ +let newState = + myFn a b c { D = d; E = e } +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = + myFn a b c { + D = d + E = e + } +""" + +[] +let ``lowercase app node with multiple args ending in a single anonymous record arg`` () = + formatSourceString + false + """ +let newState = + myFn a b c {| D = d; E = e |} +""" + { config with + RecordMultilineFormatter = NumberOfItems } + |> prepend newline + |> should + equal + """ +let newState = + myFn a b c {| + D = d + E = e + |} +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs index b330cf93b5..c3d0e79b21 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -66,12 +66,12 @@ opt { opt { let! abc = def () - and! foo = - { bar with + and! foo = { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } () } @@ -143,38 +143,6 @@ opt { } """ -[] -let ``andBang with computation expression`` () = - formatSourceString - false - """ -task { - let! abc = def () - and! meh = - task { - // comment - return 42 - } - () -} -""" - config - |> prepend newline - |> should - equal - """ -task { - let! abc = def () - - and! meh = task { - // comment - return 42 - } - - () -} -""" - [] let ``andBang with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynExprAnonRecdStructTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAnonRecdStructTests.fs new file mode 100644 index 0000000000..2f50fdf8df --- /dev/null +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAnonRecdStructTests.fs @@ -0,0 +1,35 @@ +module Fantomas.Core.Tests.Stroustrup.SynExprAnonRecdStructTests + +open NUnit.Framework +open FsUnit +open Fantomas.Core.Tests.TestHelper +open Fantomas.Core + +let config = + { config with + MultilineBracketStyle = Stroustrup } + +[] +let ``anonymous struct record with trivia`` () = + formatSourceString + false + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" + config + |> prepend newline + |> should + equal + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs index 153490a445..959701005b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -49,10 +49,10 @@ match x with equal """ match x with -| _ -> - { astContext with +| _ -> { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -104,30 +104,6 @@ match x with |} """ -[] -let ``synMatchClause in match expression with computation expression`` () = - formatSourceString - false - """ -match x with -| _ -> - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -match x with -| _ -> task { - // some computation here - () - } -""" - [] let ``synMatchClause in match expression with list`` () = formatSourceString @@ -265,10 +241,10 @@ with ex -> """ try foo () -with ex -> - { astContext with +with ex -> { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -324,33 +300,6 @@ with ex -> struct {| |} """ -[] -let ``synMatchClause in try/with expression with computation expression`` () = - formatSourceString - false - """ -try - foo() -with -| ex -> - task { - // some computation here - () - } -""" - config - |> prepend newline - |> should - equal - """ -try - foo () -with ex -> task { - // some computation here - () -} -""" - [] let ``synMatchClause in try/with expression with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs index 12058bbaa9..5edcc57e36 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``record type definition`` () = @@ -63,9 +63,6 @@ type V = // comment } """ -// TODO: I feel like stroustrup should not work when there are members involved -// Having members would require the `with` keyword which is not recommended by the style guide: https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-record-declarations - [] let ``record type definition with members`` () = formatSourceString @@ -87,11 +84,10 @@ type V = """ namespace Foo -type V = - { - X: SomeFieldType - Y: OhSomethingElse - Z: ALongTypeName - } +type V = { + X: SomeFieldType + Y: OhSomethingElse + Z: ALongTypeName +} with member Coordinate: SomeFieldType * OhSomethingElse * ALongTypeName """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs index 11e531eab4..a3b9e8e528 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs @@ -3,11 +3,11 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``record type definition`` () = @@ -77,9 +77,6 @@ type V = // comment } """ -// TODO: I feel like stroustrup should not work when there are members involved -// Having members would require the `with` keyword which is not recommended by the style guide: https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-record-declarations - [] let ``record type definition with members`` () = formatSourceString @@ -97,15 +94,36 @@ type V = |> should equal """ -type V = - { - X: SomeFieldType - Y: OhSomethingElse - Z: ALongTypeName - } +type V = { + X: SomeFieldType + Y: OhSomethingElse + Z: ALongTypeName +} with member this.Coordinate = (this.X, this.Y, this.Z) """ +[] +let ``record type definition with members and trivia`` () = + formatSourceString + false + """ +type X = { + Y: int +} with // foo + member x.Z = () +""" + { config with + NewlineBetweenTypeDefinitionAndMembers = false } + |> prepend newline + |> should + equal + """ +type X = { + Y: int +} with // foo + member x.Z = () +""" + [] let ``record definition with private accessibility modifier, 2481`` () = formatSourceString @@ -188,17 +206,36 @@ type NonEmptyList<'T> = |> should equal """ -type NonEmptyList<'T> = - private - { - List: 'T list - } +type NonEmptyList<'T> = private { + List: 'T list +} with member this.Head = this.List.Head member this.Tail = this.List.Tail member this.Length = this.List.Length """ +[] +let ``record definition with accessibility modifier without members`` () = + formatSourceString + false + """ +type NonEmptyList<'T> = + private + { List: 'T list; Value: 'T; Third: string} +""" + config + |> prepend newline + |> should + equal + """ +type NonEmptyList<'T> = private { + List: 'T list + Value: 'T + Third: string +} +""" + [] let ``outdenting problem when specifying record with accessibility modifier, 2597`` () = formatSourceString @@ -257,3 +294,63 @@ type MangaDexAtHomeResponse = { |} } """ + +[] +let ``record interface declarations can break with Stroustrup enabled, 2787 `` () = + formatSourceString + false + """ +type IEvent = interface end + +type SomeEvent = + { Id: string + Name: string } + interface IEvent + +type UpdatedName = { PreviousName: string } +""" + { config with + NewlineBetweenTypeDefinitionAndMembers = false } + |> prepend newline + |> should + equal + """ +type IEvent = + interface + end + +type SomeEvent = { + Id: string + Name: string +} with + interface IEvent + +type UpdatedName = { PreviousName: string } +""" + +[] +let ``record member declarations can break with Stroustrup enabled, 2787 `` () = + formatSourceString + false + """ +type SomeEvent = + { Id: string + Name: string } + member x.BreakWithOtherStuffAs well = () + +type UpdatedName = { PreviousName: string } +""" + { config with + NewlineBetweenTypeDefinitionAndMembers = false } + |> prepend newline + |> should + equal + """ +type SomeEvent = { + Id: string + Name: string +} with + member x.BreakWithOtherStuffAs well = () + +type UpdatedName = { PreviousName: string } +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs index e23c3b8df8..9c0c9f6cdc 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -68,19 +68,19 @@ myComp { equal """ myComp { - yield! - { bar with + yield! { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } - return! - { bar with + return! { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } } """ @@ -157,42 +157,6 @@ myComp { } """ -[] -let ``yieldOrReturnBang with computation expression`` () = - formatSourceString - false - """ -myComp { - yield! - seq { - // meh - return 0 .. 2 - } - return! - seq { - // meh - return 0 .. 2 - } -} -""" - config - |> prepend newline - |> should - equal - """ -myComp { - yield! seq { - // meh - return 0..2 - } - - return! seq { - // meh - return 0..2 - } -} -""" - [] let ``yieldOrReturnBang with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs index 6828057ab7..114d7151ae 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs @@ -2,12 +2,12 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] @@ -68,19 +68,19 @@ myComp { equal """ myComp { - yield - { bar with + yield { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } - return - { bar with + return { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } } """ @@ -157,42 +157,6 @@ myComp { } """ -[] -let ``yieldOrReturn with computation expression`` () = - formatSourceString - false - """ -myComp { - yield - seq { - // meh - return 0 .. 2 - } - return - seq { - // meh - return 0 .. 2 - } -} -""" - config - |> prepend newline - |> should - equal - """ -myComp { - yield seq { - // meh - return 0..2 - } - - return seq { - // meh - return 0..2 - } -} -""" - [] let ``yieldOrReturn with list`` () = formatSourceString diff --git a/src/Fantomas.Core.Tests/StructTests.fs b/src/Fantomas.Core.Tests/StructTests.fs index 88f53a7821..6420b0d6e2 100644 --- a/src/Fantomas.Core.Tests/StructTests.fs +++ b/src/Fantomas.Core.Tests/StructTests.fs @@ -164,3 +164,28 @@ type NameStruct() = struct end """ + +[] +let ``anonymous struct record with trivia`` () = + formatSourceString + false + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" + config + |> prepend newline + |> should + equal + """ +struct // 1 + {| // 2 + // 3 + X = 4 + // 5 + |} // 6 +""" diff --git a/src/Fantomas.Core.Tests/SynConstTests.fs b/src/Fantomas.Core.Tests/SynConstTests.fs index 33bac8b930..f111fbd6a0 100644 --- a/src/Fantomas.Core.Tests/SynConstTests.fs +++ b/src/Fantomas.Core.Tests/SynConstTests.fs @@ -705,12 +705,12 @@ a:hover {color: #ecc;} [] let ``verbatim string in AST is preserved, 560`` () = - formatSourceString + formatAST false """ let s = @"\" """ - { config with StrictMode = true } + config |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/SynLongIdentTests.fs b/src/Fantomas.Core.Tests/SynLongIdentTests.fs index 9e19ddb53a..0c26795e12 100644 --- a/src/Fantomas.Core.Tests/SynLongIdentTests.fs +++ b/src/Fantomas.Core.Tests/SynLongIdentTests.fs @@ -401,7 +401,6 @@ let ``backticks can be added from AST only scenarios`` () = tree, config = { config with - StrictMode = true InsertFinalNewline = false } ) |> Async.RunSynchronously diff --git a/src/Fantomas.Core.Tests/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index cc35bd77df..52ea5b0c1f 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -1,11 +1,9 @@ module Fantomas.Core.Tests.TestHelper open System -open Fantomas.Core.SyntaxOak +open Fantomas.Core open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig -open Fantomas.Core [] do () @@ -26,23 +24,31 @@ let private safeToIgnoreWarnings = let formatSourceString isFsiFile (s: string) config = async { - let! formatted = - if not config.StrictMode then - CodeFormatter.FormatDocumentAsync(isFsiFile, s, config) - else - let ast, _ = - Fantomas.FCS.Parse.parseFile isFsiFile (FSharp.Compiler.Text.SourceText.ofString s) [] + let! formatted = CodeFormatter.FormatDocumentAsync(isFsiFile, s, config) + let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted.Code) - CodeFormatter.FormatASTAsync(ast, config = config) + if not isValid then + failwithf $"The formatted result is not valid F# code or contains warnings\n%s{formatted.Code}" - let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted) + return formatted.Code.Replace("\r\n", "\n") + } + + |> Async.RunSynchronously + +/// The `source` will first be parsed to AST. +let formatAST isFsiFile (source: string) config = + async { + let ast, _ = + Fantomas.FCS.Parse.parseFile isFsiFile (FSharp.Compiler.Text.SourceText.ofString source) [] + + let! formattedCode = CodeFormatter.FormatASTAsync(ast, config = config) + let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formattedCode) if not isValid then - failwithf $"The formatted result is not valid F# code or contains warnings\n%s{formatted}" + failwithf $"The formatted result is not valid F# code or contains warnings\n%s{formattedCode}" - return formatted.Replace("\r\n", "\n") + return formattedCode.Replace("\r\n", "\n") } - |> Async.RunSynchronously let formatSourceStringWithDefines defines (s: string) config = @@ -55,22 +61,21 @@ let formatSourceStringWithDefines defines (s: string) config = let! asts = CodeFormatterImpl.parse false source let ast = - Array.filter (fun (_, d: DefineCombination) -> List.sort d = List.sort defines) asts + Array.filter (fun (_, DefineCombination(d)) -> List.sort d = List.sort defines) asts |> Array.head |> fst - return CodeFormatterImpl.formatAST ast (Some source) config + return CodeFormatterImpl.formatAST ast (Some source) config None } |> Async.RunSynchronously + let defines = DefineCombination(defines) + // merge with itself to make #if go on beginning of line - let _, fragments = - String.splitInFragments config.EndOfLine.NewLineString [ (defines, result) ] - |> List.head + let mergedFormatResult = + MultipleDefineCombinations.mergeMultipleFormatResults config [ (defines, result); (defines, result) ] - String.merge fragments fragments - |> String.concat config.EndOfLine.NewLineString - |> String.normalizeNewLine + String.normalizeNewLine mergedFormatResult.Code let isValidFSharpCode isFsiFile s = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, s) |> Async.RunSynchronously @@ -84,11 +89,6 @@ let equal x = equal x let inline prepend s content = s + content - -let formatConfig = - { FormatConfig.Default with - StrictMode = true } - let (==) actual expected = Assert.AreEqual(expected, actual) let fail () = Assert.Fail() let pass () = Assert.Pass() diff --git a/src/Fantomas.Core.Tests/TypeDeclarationTests.fs b/src/Fantomas.Core.Tests/TypeDeclarationTests.fs index cd31210c13..92c8155473 100644 --- a/src/Fantomas.Core.Tests/TypeDeclarationTests.fs +++ b/src/Fantomas.Core.Tests/TypeDeclarationTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.TypeDeclarationTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``exception declarations`` () = diff --git a/src/Fantomas.Core.Tests/TypeProviderTests.fs b/src/Fantomas.Core.Tests/TypeProviderTests.fs index 5c16cee979..1fd7a39e8e 100644 --- a/src/Fantomas.Core.Tests/TypeProviderTests.fs +++ b/src/Fantomas.Core.Tests/TypeProviderTests.fs @@ -1,5 +1,6 @@ module Fantomas.Core.Tests.TypeProviderTests +open Fantomas.Core open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper @@ -49,7 +50,7 @@ type Graphml = XmlProvider] let ``should throw FormatException on unparsed input`` () = - Assert.Throws(fun () -> + Assert.Throws(fun () -> formatSourceString false """ diff --git a/src/Fantomas.Core.Tests/UnionTests.fs b/src/Fantomas.Core.Tests/UnionTests.fs index dd0e7aa303..9315d9e66d 100644 --- a/src/Fantomas.Core.Tests/UnionTests.fs +++ b/src/Fantomas.Core.Tests/UnionTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.UnionsTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``enums declaration`` () = @@ -203,7 +203,7 @@ let main argv = [] let ``enums conversion with strict mode`` () = - formatSourceString + formatAST false """ type uColor = @@ -211,7 +211,7 @@ type uColor = | Green = 1u | Blue = 2u let col3 = Microsoft.FSharp.Core.LanguagePrimitives.EnumOfValue(2u)""" - { config with StrictMode = true } + config |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/UtilsTests.fs b/src/Fantomas.Core.Tests/UtilsTests.fs index 92b91bef9f..47c2968229 100644 --- a/src/Fantomas.Core.Tests/UtilsTests.fs +++ b/src/Fantomas.Core.Tests/UtilsTests.fs @@ -1,125 +1,9 @@ module Fantomas.Core.Tests.UtilsTests -open System open NUnit.Framework open Fantomas.Core -open Fantomas.Core.Tests.TestHelper open FsCheck -let private mergeAndCompare a b expected = - let result = - let getFragments code = - String.splitInFragments config.EndOfLine.NewLineString [ code ] - |> List.head - |> snd - - String.merge (getFragments a) (getFragments b) - |> String.concat Environment.NewLine - |> String.normalizeNewLine - - let normalizedExpected = String.normalizeNewLine expected - normalizedExpected == result - -[] -let ``merging of source code that starts with a hash`` () = - let a = - """#if NOT_DEFINED - printfn \"meh\" -#else - -#endif -""" - - let b = - """#if NOT_DEFINED - -#else - printfn \"foo\" -#endif -""" - - """#if NOT_DEFINED - printfn \"meh\" -#else - printfn \"foo\" -#endif -""" - |> mergeAndCompare ([], a) ([ "NOT_DEFINED" ], b) - -[] -let ``merging of defines content work when source code starts with a newline`` () = - let a = - """ -[] -let private assemblyConfig() = - #if TRACE - - #else - let x = "x" - #endif - x -""" - - let b = - """ -[] -let private assemblyConfig() = - #if TRACE - let x = "" - #else - - #endif - x -""" - - """ -[] -let private assemblyConfig() = -#if TRACE - let x = "" -#else - let x = "x" -#endif - x -""" - |> mergeAndCompare ([], a) ([ "TRACE" ], b) - -[] -let ``only split on control structure keyword`` () = - let a = - """ -#if INTERACTIVE -#else -#load "../FSharpx.TypeProviders/SetupTesting.fsx" - -SetupTesting.generateSetupScript __SOURCE_DIRECTORY__ - -#load "__setup__.fsx" -#endif -""" - - let b = - """ -#if INTERACTIVE -#else - - - -#endif - """ - - """ -#if INTERACTIVE -#else -#load "../FSharpx.TypeProviders/SetupTesting.fsx" - -SetupTesting.generateSetupScript __SOURCE_DIRECTORY__ - -#load "__setup__.fsx" -#endif -""" - |> mergeAndCompare ([], a) ([ "INTERACTIVE" ], b) - [] let ``when input is empty`` () = let property (p: bool) : bool = diff --git a/src/Fantomas.Core/ASTTransformer.fs b/src/Fantomas.Core/ASTTransformer.fs index ab4e21fcfd..5d0bcb954a 100644 --- a/src/Fantomas.Core/ASTTransformer.fs +++ b/src/Fantomas.Core/ASTTransformer.fs @@ -952,14 +952,6 @@ let mkExpr (creationAide: CreationAide) (e: SynExpr) : Expr = ExprArrayOrListNode(o, [ mkExpr creationAide singleExpr ], c, exprRange) |> Expr.ArrayOrList | SynExpr.Record(baseInfo, copyInfo, recordFields, StartEndRange 1 (mOpen, _, mClose)) -> - let extra = - match baseInfo, copyInfo with - | Some _, Some _ -> failwith "Unexpected that both baseInfo and copyInfo are present in SynExpr.Record" - | Some(t, e, mInherit, _, m), None -> - mkInheritConstructor creationAide t e mInherit m |> RecordNodeExtra.Inherit - | None, Some(copyExpr, _) -> mkExpr creationAide copyExpr |> RecordNodeExtra.With - | None, None -> RecordNodeExtra.None - let fieldNodes = recordFields |> List.choose (function @@ -968,26 +960,68 @@ let mkExpr (creationAide: CreationAide) (e: SynExpr) : Expr = Some(RecordFieldNode(mkSynLongIdent fieldName, stn "=" mEq, mkExpr creationAide expr, m)) | _ -> None) - ExprRecordNode(stn "{" mOpen, extra, fieldNodes, stn "}" mClose, exprRange) - |> Expr.Record - | SynExpr.AnonRecd(isStruct, copyInfo, recordFields, EndRange 2 (mClose, _), trivia) -> + match baseInfo, copyInfo with + | Some _, Some _ -> failwith "Unexpected that both baseInfo and copyInfo are present in SynExpr.Record" + | Some(t, e, mInherit, _, m), None -> + let inheritCtor = mkInheritConstructor creationAide t e mInherit m + + ExprInheritRecordNode(stn "{" mOpen, inheritCtor, fieldNodes, stn "}" mClose, exprRange) + |> Expr.InheritRecord + | None, Some(copyExpr, _) -> + let copyExpr = mkExpr creationAide copyExpr + + ExprRecordNode(stn "{" mOpen, Some copyExpr, fieldNodes, stn "}" mClose, exprRange) + |> Expr.Record + | None, None -> + ExprRecordNode(stn "{" mOpen, None, fieldNodes, stn "}" mClose, exprRange) + |> Expr.Record + | SynExpr.AnonRecd(true, + copyInfo, + recordFields, + (StartRange 6 (mStruct, _) & EndRange 2 (mClose, _)), + { OpeningBraceRange = mOpen }) -> let fields = recordFields |> List.choose (function | ident, Some mEq, e -> let m = unionRanges ident.idRange e.Range - Some(AnonRecordFieldNode(mkIdent ident, stn "=" mEq, mkExpr creationAide e, m)) + + let longIdent = + IdentListNode([ IdentifierOrDot.Ident(mkIdent ident) ], ident.idRange) + + Some(RecordFieldNode(longIdent, stn "=" mEq, mkExpr creationAide e, m)) | _ -> None) - ExprAnonRecordNode( - isStruct, - stn "{|" trivia.OpeningBraceRange, + ExprAnonStructRecordNode( + stn "struct" mStruct, + stn "{|" mOpen, Option.map (fst >> mkExpr creationAide) copyInfo, fields, stn "|}" mClose, exprRange ) - |> Expr.AnonRecord + |> Expr.AnonStructRecord + | SynExpr.AnonRecd(false, copyInfo, recordFields, EndRange 2 (mClose, _), { OpeningBraceRange = mOpen }) -> + let fields = + recordFields + |> List.choose (function + | ident, Some mEq, e -> + let m = unionRanges ident.idRange e.Range + + let longIdent = + IdentListNode([ IdentifierOrDot.Ident(mkIdent ident) ], ident.idRange) + + Some(RecordFieldNode(longIdent, stn "=" mEq, mkExpr creationAide e, m)) + | _ -> None) + + ExprRecordNode( + stn "{|" mOpen, + Option.map (fst >> mkExpr creationAide) copyInfo, + fields, + stn "|}" mClose, + exprRange + ) + |> Expr.Record | SynExpr.ObjExpr(t, eio, withKeyword, bd, members, ims, StartRange 3 (mNew, _), StartEndRange 1 (mOpen, _, mClose)) -> let interfaceNodes = ims diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 83573c0a31..c808c51f13 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -2,27 +2,56 @@ namespace Fantomas.Core open FSharp.Compiler.Syntax open FSharp.Compiler.Text +open Fantomas.Core.SyntaxOak [] type CodeFormatter = static member ParseAsync(isSignature, source) : Async<(ParsedInput * string list) array> = - CodeFormatterImpl.getSourceText source |> CodeFormatterImpl.parse isSignature + async { + let! results = CodeFormatterImpl.getSourceText source |> CodeFormatterImpl.parse isSignature + return results |> Array.map (fun (ast, DefineCombination(defines)) -> ast, defines) + } - static member FormatASTAsync(ast: ParsedInput, ?source, ?config) : Async = - let sourceText = Option.map CodeFormatterImpl.getSourceText source - let config = Option.defaultValue FormatConfig.FormatConfig.Default config + static member FormatASTAsync(ast: ParsedInput) : Async = + async { + let result = CodeFormatterImpl.formatAST ast None FormatConfig.Default None + return result.Code + } - CodeFormatterImpl.formatAST ast sourceText config |> async.Return + static member FormatASTAsync(ast: ParsedInput, config) : Async = + async { + let result = CodeFormatterImpl.formatAST ast None config None + return result.Code + } + + static member FormatASTAsync(ast: ParsedInput, source) : Async = + async { + let sourceText = Some(CodeFormatterImpl.getSourceText source) + let result = CodeFormatterImpl.formatAST ast sourceText FormatConfig.Default None + return result.Code + } + + static member FormatASTAsync(ast: ParsedInput, source, config) : Async = + async { + let sourceText = Some(CodeFormatterImpl.getSourceText source) + let result = CodeFormatterImpl.formatAST ast sourceText config None + return result + } + + static member FormatDocumentAsync(isSignature, source) = + CodeFormatterImpl.formatDocument FormatConfig.Default isSignature (CodeFormatterImpl.getSourceText source) None static member FormatDocumentAsync(isSignature, source, config) = - let config = Option.defaultValue FormatConfig.FormatConfig.Default config + CodeFormatterImpl.formatDocument config isSignature (CodeFormatterImpl.getSourceText source) None + static member FormatDocumentAsync(isSignature, source, config, cursor) = + CodeFormatterImpl.formatDocument config isSignature (CodeFormatterImpl.getSourceText source) (Some cursor) + + static member FormatSelectionAsync(isSignature, source, selection) = CodeFormatterImpl.getSourceText source - |> CodeFormatterImpl.formatDocument config isSignature + |> Selection.formatSelection FormatConfig.Default isSignature selection static member FormatSelectionAsync(isSignature, source, selection, config) = - let config = Option.defaultValue FormatConfig.FormatConfig.Default config - CodeFormatterImpl.getSourceText source |> Selection.formatSelection config isSignature selection @@ -33,3 +62,31 @@ type CodeFormatter = static member MakeRange(fileName, startLine, startCol, endLine, endCol) = Range.mkRange fileName (Position.mkPos startLine startCol) (Position.mkPos endLine endCol) + + static member MakePosition(line, column) = Position.mkPos line column + + static member ParseOakAsync(isSignature: bool, source: string) : Async<(Oak * string list) array> = + async { + let sourceText = CodeFormatterImpl.getSourceText source + let! ast = CodeFormatterImpl.parse isSignature sourceText + + return + ast + |> Array.map (fun (ast, defines) -> + let oak = ASTTransformer.mkOak (Some sourceText) ast + oak, defines.Value) + } + + static member FormatOakAsync(oak: Oak) : Async = + async { + let context = Context.Context.Create false FormatConfig.Default + let result = context |> CodePrinter.genFile oak |> Context.dump false + return result.Code + } + + static member FormatOakAsync(oak: Oak, config: FormatConfig) : Async = + async { + let context = Context.Context.Create false config + let result = context |> CodePrinter.genFile oak |> Context.dump false + return result.Code + } diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index dc8e37e84a..edf6cfb71e 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -1,26 +1,58 @@ namespace Fantomas.Core -open Fantomas.Core.FormatConfig open FSharp.Compiler.Text open FSharp.Compiler.Syntax +open Fantomas.Core.SyntaxOak [] type CodeFormatter = - // /// Parse a source string using given config + /// Parse a source string using given config static member ParseAsync: isSignature: bool * source: string -> Async<(ParsedInput * string list) array> - /// Format an abstract syntax tree using an optional source for trivia processing - static member FormatASTAsync: ast: ParsedInput * ?source: string * ?config: FormatConfig -> Async + /// Format an abstract syntax tree + static member FormatASTAsync: ast: ParsedInput -> Async - /// Format a source string using an optional config - static member FormatDocumentAsync: isSignature: bool * source: string * ?config: FormatConfig -> Async + /// Format an abstract syntax tree using a given config + static member FormatASTAsync: ast: ParsedInput * config: FormatConfig -> Async - // /// Format a part of source string using given config, and return the (formatted) selected part only. - // /// Beware that the range argument is inclusive. The closest expression inside the selection will be formatted if possible. + /// Format an abstract syntax tree with the original source for trivia processing + static member FormatASTAsync: ast: ParsedInput * source: string -> Async + + /// + /// Format a source string using an optional config. + /// + /// Determines whether the F# parser will process the source as signature file. + /// F# source code + static member FormatDocumentAsync: isSignature: bool * source: string -> Async + + /// + /// Format a source string using an optional config. + /// + /// Determines whether the F# parser will process the source as signature file. + /// F# source code + /// Fantomas configuration + static member FormatDocumentAsync: isSignature: bool * source: string * config: FormatConfig -> Async + + /// + /// Format a source string using an optional config. + /// + /// Determines whether the F# parser will process the source as signature file. + /// F# source code + /// Fantomas configuration + /// The location of a cursor, zero-based. + static member FormatDocumentAsync: + isSignature: bool * source: string * config: FormatConfig * cursor: pos -> Async + + /// Format a part of a source string and return the (formatted) selected part only. + /// Beware that the range argument is inclusive. The closest expression inside the selection will be formatted if possible. + static member FormatSelectionAsync: isSignature: bool * source: string * selection: range -> Async + + /// Format a part of source string using given config, and return the (formatted) selected part only. + /// Beware that the range argument is inclusive. The closest expression inside the selection will be formatted if possible. static member FormatSelectionAsync: - isSignature: bool * source: string * selection: Range * ?config: FormatConfig -> Async + isSignature: bool * source: string * selection: range * config: FormatConfig -> Async - // /// Check whether an input string is invalid in F# by attempting to parse the code. + /// Check whether an input string is invalid in F# by attempting to parse the code. static member IsValidFSharpCodeAsync: isSignature: bool * source: string -> Async /// Returns the version of Fantomas found in the AssemblyInfo @@ -28,3 +60,15 @@ type CodeFormatter = /// Make a range from (startLine, startCol) to (endLine, endCol) to select some text static member MakeRange: fileName: string * startLine: int * startCol: int * endLine: int * endCol: int -> range + + /// Make a pos from line and column + static member MakePosition: line: int * column: int -> pos + + /// Parse a source string to SyntaxOak + static member ParseOakAsync: isSignature: bool * source: string -> Async<(Oak * string list) array> + + /// Format SyntaxOak to string + static member FormatOakAsync: oak: Oak -> Async + + /// Format SyntaxOak to string using given config + static member FormatOakAsync: oak: Oak * config: FormatConfig -> Async diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 15bfd0ed58..f051473709 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -4,9 +4,7 @@ module internal Fantomas.Core.CodeFormatterImpl open FSharp.Compiler.Diagnostics open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open Fantomas.Core -open Fantomas.Core.FormatConfig -open Fantomas.Core.SyntaxOak +open MultipleDefineCombinations let getSourceText (source: string) : ISourceText = source.TrimEnd() |> SourceText.ofString @@ -28,9 +26,9 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin |> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) if not errors.IsEmpty then - raise (FormatException $"Parsing failed with errors: %A{baseDiagnostics}") + raise (ParseException baseDiagnostics) - return [| (baseUntypedTree, []) |] + return [| (baseUntypedTree, DefineCombination.Empty) |] } | hashDirectives -> let defineCombinations = Defines.getDefineCombination hashDirectives @@ -39,36 +37,48 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin |> List.map (fun defineCombination -> async { let untypedTree, diagnostics = - Fantomas.FCS.Parse.parseFile isSignature source defineCombination + Fantomas.FCS.Parse.parseFile isSignature source defineCombination.Value let errors = diagnostics |> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) if not errors.IsEmpty then - raise (FormatException $"Parsing failed with errors: %A{diagnostics}") + raise (ParseException diagnostics) return (untypedTree, defineCombination) }) |> Async.Parallel /// Format an abstract syntax tree using given config -let formatAST (ast: ParsedInput) (sourceText: ISourceText option) (config: FormatConfig) : string = - let formattedSourceCode = - let context = Context.Context.Create config - - let fileNode = - match sourceText with - | None -> ASTTransformer.mkOak None ast - | Some sourceText -> - ASTTransformer.mkOak (Some sourceText) ast - |> Trivia.enrichTree config sourceText ast - - context |> CodePrinter.genFile fileNode |> Context.dump false - - formattedSourceCode - -let format (config: FormatConfig) (isSignature: bool) (source: ISourceText) : Async = +let formatAST + (ast: ParsedInput) + (sourceText: ISourceText option) + (config: FormatConfig) + (cursor: pos option) + : FormatResult = + let context = Context.Context.Create sourceText.IsSome config + + let oak = + match sourceText with + | None -> ASTTransformer.mkOak None ast + | Some sourceText -> + ASTTransformer.mkOak (Some sourceText) ast + |> Trivia.enrichTree config sourceText ast + + let oak = + match cursor with + | None -> oak + | Some cursor -> Trivia.insertCursor oak cursor + + context |> CodePrinter.genFile oak |> Context.dump false + +let formatDocument + (config: FormatConfig) + (isSignature: bool) + (source: ISourceText) + (cursor: pos option) + : Async = async { let! asts = parse isSignature source @@ -76,7 +86,7 @@ let format (config: FormatConfig) (isSignature: bool) (source: ISourceText) : As asts |> Array.map (fun (ast', defineCombination) -> async { - let formattedCode = formatAST ast' (Some source) config + let formattedCode = formatAST ast' (Some source) config cursor return (defineCombination, formattedCode) }) |> Async.Parallel @@ -86,36 +96,7 @@ let format (config: FormatConfig) (isSignature: bool) (source: ISourceText) : As match results with | [] -> failwith "not possible" | [ (_, x) ] -> x - | all -> - let allInFragments = all |> String.splitInFragments config.EndOfLine.NewLineString - - let allHaveSameFragmentCount = - let allWithCount = List.map (fun (_, f: string list) -> f.Length) allInFragments - - (Set allWithCount).Count = 1 - - if not allHaveSameFragmentCount then - let chunkReport = - allInFragments - |> List.map (fun (defines, fragments) -> - sprintf "[%s] has %i fragments" (String.concat ", " defines) fragments.Length) - |> String.concat config.EndOfLine.NewLineString - - raise ( - FormatException( - $"""Fantomas is trying to format the input multiple times due to the detect of multiple defines. -There is a problem with merging all the code back together. -{chunkReport} -Please raise an issue at https://fsprojects.github.io/fantomas-tools/#/fantomas/preview.""" - ) - ) - - List.map snd allInFragments - |> List.reduce String.merge - |> String.concat config.EndOfLine.NewLineString + | all -> mergeMultipleFormatResults config all return merged } - -/// Format a source string using given config -let formatDocument config isSignature source = format config isSignature source diff --git a/src/Fantomas.Core/CodeFormatterImpl.fsi b/src/Fantomas.Core/CodeFormatterImpl.fsi index bbf9ea6783..83fbb373cb 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fsi +++ b/src/Fantomas.Core/CodeFormatterImpl.fsi @@ -3,13 +3,13 @@ module internal Fantomas.Core.CodeFormatterImpl open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open Fantomas.Core.FormatConfig -open Fantomas.Core.SyntaxOak val getSourceText: source: string -> ISourceText -val formatAST: ast: ParsedInput -> sourceText: ISourceText option -> config: FormatConfig -> string +val formatAST: + ast: ParsedInput -> sourceText: ISourceText option -> config: FormatConfig -> cursor: pos option -> FormatResult val parse: isSignature: bool -> source: ISourceText -> Async<(ParsedInput * DefineCombination)[]> -val formatDocument: config: FormatConfig -> isSignature: bool -> source: ISourceText -> Async +val formatDocument: + config: FormatConfig -> isSignature: bool -> source: ISourceText -> cursor: pos option -> Async diff --git a/src/Fantomas.Core/CodeFormatterTypes.fs b/src/Fantomas.Core/CodeFormatterTypes.fs new file mode 100644 index 0000000000..13e536e24c --- /dev/null +++ b/src/Fantomas.Core/CodeFormatterTypes.fs @@ -0,0 +1,12 @@ +namespace Fantomas.Core + +open FSharp.Compiler.Text + +type FormatResult = + { + /// Formatted code + Code: string + /// New position of the input cursor. + /// This can be None when no cursor was passed as input or no position was resolved. + Cursor: pos option + } diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 5ddb4f4b56..1e9711bf94 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -3,7 +3,6 @@ module internal rec Fantomas.Core.CodePrinter open System open Fantomas.Core.Context open Fantomas.Core.SyntaxOak -open Fantomas.Core.FormatConfig open Microsoft.FSharp.Core.CompilerServices let noBreakInfixOps = set [| "="; ">"; "<"; "%" |] @@ -68,7 +67,7 @@ let (|ParenExpr|_|) (e: Expr) = | Expr.Constant(Constant.Unit _) -> Some e | _ -> None -let genTrivia (trivia: TriviaNode) (ctx: Context) = +let genTrivia (node: Node) (trivia: TriviaNode) (ctx: Context) = let currentLastLine = ctx.WriterModel.Lines |> List.tryHead // Some items like #if or Newline should be printed on a newline @@ -97,12 +96,45 @@ let genTrivia (trivia: TriviaNode) (ctx: Context) = | CommentOnSingleLine s | Directive s -> (ifElse addNewline sepNlnForTrivia sepNone) +> !-s +> sepNlnForTrivia | Newline -> (ifElse addNewline (sepNlnForTrivia +> sepNlnForTrivia) sepNlnForTrivia) + | Cursor -> + fun ctx -> + // TODO: this assumes the cursor is placed on the same line as the EndLine of the Node. + let originalColumnOffset = trivia.Range.EndColumn - node.Range.EndColumn + + let formattedCursor = + FSharp.Compiler.Text.Position.mkPos ctx.WriterModel.Lines.Length (ctx.Column + originalColumnOffset) + + { ctx with + FormattedCursor = Some formattedCursor } gen ctx -let enterNode<'n when 'n :> Node> (n: 'n) = col sepNone n.ContentBefore genTrivia -let leaveNode<'n when 'n :> Node> (n: 'n) = col sepNone n.ContentAfter genTrivia -let genNode<'n when 'n :> Node> (n: 'n) (f: Context -> Context) = enterNode n +> f +> leaveNode n +let recordCursorNode f (node: Node) (ctx: Context) = + match node.TryGetCursor with + | None -> f ctx + | Some cursor -> + // TODO: this currently assume the node fits on a single line. + // This won't be accurate in case of a multiline string. + let currentStartLine = ctx.WriterModel.Lines.Length + let currentStartColumn = ctx.Column + + let ctxAfter = f ctx + + let formattedCursor = + let columnOffsetInSource = cursor.Column - node.Range.StartColumn + FSharp.Compiler.Text.Position.mkPos currentStartLine (currentStartColumn + columnOffsetInSource) + + { ctxAfter with + FormattedCursor = Some formattedCursor } + +let enterNode<'n when 'n :> Node> (n: 'n) = + col sepNone n.ContentBefore (genTrivia n) + +let leaveNode<'n when 'n :> Node> (n: 'n) = + col sepNone n.ContentAfter (genTrivia n) + +let genNode<'n when 'n :> Node> (n: 'n) (f: Context -> Context) = + enterNode n +> recordCursorNode f n +> leaveNode n let genSingleTextNode (node: SingleTextNode) = !-node.Text |> genNode node @@ -361,282 +393,60 @@ let genExpr (e: Expr) = +> genTuple node.Tuple +> genSingleTextNode node.ClosingParen |> genNode node - | Expr.ArrayOrList node -> - if node.Elements.IsEmpty then - genSingleTextNode node.Opening +> genSingleTextNode node.Closing |> genNode node - else - let smallExpression = - genSingleTextNode node.Opening - +> addSpaceIfSpaceAroundDelimiter - +> col sepSemi node.Elements genExpr - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.Closing - - let multilineExpression = - let genMultiLineArrayOrListAlignBrackets = - genSingleTextNode node.Opening - +> indent - +> sepNlnUnlessLastEventIsNewline - +> col sepNln node.Elements genExpr - +> unindent - +> sepNlnUnlessLastEventIsNewline - +> genSingleTextNode node.Closing - - let genMultiLineArrayOrList = - genSingleTextNodeSuffixDelimiter node.Opening - +> atCurrentColumnIndent ( - sepNlnWhenWriteBeforeNewlineNotEmpty - +> col sepNln node.Elements genExpr - +> (enterNode node.Closing - +> (fun ctx -> - let isFixed = lastWriteEventIsNewline ctx - (onlyIfNot isFixed sepSpace +> !-node.Closing.Text +> leaveNode node.Closing) ctx)) - ) - - ifAlignOrStroustrupBrackets genMultiLineArrayOrListAlignBrackets genMultiLineArrayOrList - - fun ctx -> - let alwaysMultiline = - let isIfThenElseWithYieldReturn e = - let (|YieldLikeExpr|_|) e = - match e with - | Expr.Single singleNode -> - if singleNode.Leading.Text.StartsWith("yield") then - Some e - else - None - | _ -> None - - match e with - | Expr.IfThen ifThenNode -> - match ifThenNode.ThenExpr with - | YieldLikeExpr _ -> true - | _ -> false - | Expr.IfThenElse ifThenElseNode -> - match ifThenElseNode.IfExpr, ifThenElseNode.ElseExpr with - | YieldLikeExpr _, _ - | _, YieldLikeExpr _ -> true - | _ -> false - | _ -> false - - List.exists isIfThenElseWithYieldReturn node.Elements - || List.forall isSynExprLambdaOrIfThenElse node.Elements - - if alwaysMultiline then - multilineExpression ctx - else - let size = getListOrArrayExprSize ctx ctx.Config.MaxArrayOrListWidth node.Elements - isSmallExpression size smallExpression multilineExpression ctx - |> genNode node + | Expr.ArrayOrList node -> fun ctx -> genArrayOrList (ctx.Config.MultilineBracketStyle = Cramped) node ctx | Expr.Record node -> - let genRecordFieldName (node: RecordFieldNode) = - genIdentListNode node.FieldName - +> sepSpace - +> genSingleTextNode node.Equals - +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr - |> genNode node + let smallRecordExpr = genSmallRecordNode node + let multilineRecordExpr = genMultilineRecord node + genRecord smallRecordExpr multilineRecordExpr node + | Expr.AnonStructRecord node -> + let genStructPrefix = genSingleTextNodeWithSpaceSuffix sepSpace node.Struct + let smallRecordExpr = genStructPrefix +> genSmallRecordNode node - let fieldsExpr = col sepNln node.Fields genRecordFieldName - let hasFields = List.isNotEmpty node.Fields + let multilineRecordExpr = + if node.Struct.HasContentAfter then + genStructPrefix +> indentSepNlnUnindent (genMultilineRecord node) + else + genStructPrefix +> genMultilineRecord node + + genRecord smallRecordExpr multilineRecordExpr node + | Expr.InheritRecord node -> + let genSmallInheritRecordExpr = + genSmallRecordBaseExpr + ((genSingleTextNode node.InheritConstructor.InheritKeyword + +> sepSpace + +> genInheritConstructor node.InheritConstructor + |> genNode (InheritConstructor.Node node.InheritConstructor)) + +> onlyIf node.HasFields sepSemi) + node - let smallRecordExpr = - genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> match node.Extra with - | RecordNodeExtra.Inherit ie -> - (genSingleTextNode ie.InheritKeyword +> sepSpace +> genInheritConstructor ie - |> genNode (InheritConstructor.Node ie)) - +> onlyIf hasFields sepSemi - | RecordNodeExtra.With we -> genExpr we +> !- " with " - | RecordNodeExtra.None -> sepNone - +> col sepSemi node.Fields genRecordFieldName - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.ClosingBrace + let genMultilineInheritRecordExpr = + let fieldsExpr = genMultilineRecordFieldsExpr node - let multilineRecordExpr = - let genMultilineRecordInstanceAlignBrackets = - match node.Extra with - | RecordNodeExtra.Inherit ie -> - genSingleTextNode node.OpeningBrace - +> indentSepNlnUnindent ( - (genSingleTextNode ie.InheritKeyword - +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genInheritConstructor ie) - |> genNode (InheritConstructor.Node ie)) - +> onlyIf hasFields sepNln - +> fieldsExpr - ) - +> sepNln - +> genSingleTextNode node.ClosingBrace - | RecordNodeExtra.With we -> - genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> atCurrentColumnIndent (genExpr we) - +> !- " with" - +> indent - +> whenShortIndent indent - +> sepNln - +> fieldsExpr - +> unindent - +> whenShortIndent unindent - +> sepNln - +> genSingleTextNode node.ClosingBrace - | RecordNodeExtra.None -> - genSingleTextNode node.OpeningBrace - +> indentSepNlnUnindent fieldsExpr - +> ifElseCtx lastWriteEventIsNewline sepNone sepNln - +> genSingleTextNode node.ClosingBrace + let genInheritInfo = + (genSingleTextNode node.InheritConstructor.InheritKeyword + +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genInheritConstructor node.InheritConstructor) + |> genNode (InheritConstructor.Node node.InheritConstructor)) + +> onlyIf node.HasFields sepNln - let genMultilineRecordInstance = - match node.Extra with - | RecordNodeExtra.Inherit ie -> - genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> atCurrentColumn ( - (genSingleTextNode ie.InheritKeyword - +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genInheritConstructor ie) - |> genNode (InheritConstructor.Node ie)) - +> onlyIf hasFields sepNln - +> fieldsExpr - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.ClosingBrace - ) - | RecordNodeExtra.With we -> - genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> atCurrentColumnIndent (genExpr we) - +> !- " with" - +> indent - +> whenShortIndent indent - +> sepNln + let genMultilineAlignBrackets = + genSingleTextNode node.OpeningBrace + +> indentSepNlnUnindent (genInheritInfo +> fieldsExpr) + +> sepNln + +> genSingleTextNode node.ClosingBrace + + let genMultilineCramped = + genSingleTextNode node.OpeningBrace + +> addSpaceIfSpaceAroundDelimiter + +> atCurrentColumn ( + genInheritInfo +> fieldsExpr - +> unindent - +> whenShortIndent unindent +> addSpaceIfSpaceAroundDelimiter +> genSingleTextNode node.ClosingBrace - | RecordNodeExtra.None -> - fun (ctx: Context) -> - let expressionStartColumn = ctx.Column - // position after `{ ` or `{` - let targetColumn = - expressionStartColumn + (if ctx.Config.SpaceAroundDelimiter then 2 else 1) - - atCurrentColumn - (genSingleTextNodeSuffixDelimiter node.OpeningBrace - +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> col sepNln node.Fields (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 e)) - +> sepNlnWhenWriteBeforeNewlineNotEmpty - +> (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 brace = - addFixedSpaces expressionStartColumn +> genSingleTextNode node.ClosingBrace - - ifElseCtx lastWriteEventIsNewline brace (addSpaceIfSpaceAroundDelimiter +> brace) ctx)) - ctx - - ifAlignOrStroustrupBrackets genMultilineRecordInstanceAlignBrackets genMultilineRecordInstance - - fun ctx -> - let size = getRecordSize ctx node.Fields - genNode node (isSmallExpression size smallRecordExpr multilineRecordExpr) ctx - | Expr.AnonRecord node -> - let genAnonRecordFieldName (node: AnonRecordFieldNode) = - genSingleTextNode node.Ident - +> sepSpace - +> genSingleTextNode node.Equals - +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr - |> genNode node - - let smallExpression = - onlyIf node.IsStruct !- "struct " - +> genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> optSingle (fun e -> genExpr e +> !- " with ") node.CopyInfo - +> col sepSemi node.Fields genAnonRecordFieldName - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.ClosingBrace - - let longExpression = - let fieldsExpr = col sepNln node.Fields genAnonRecordFieldName - - let genMultilineAnonRecord = - let recordExpr = - match node.CopyInfo with - | Some e -> - genSingleTextNodeSuffixDelimiter node.OpeningBrace - +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> atCurrentColumn (genExpr e +> (!- " with" +> indentSepNlnUnindent fieldsExpr)) - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.ClosingBrace - | None -> - fun ctx -> - // position after `{| ` or `{|` - let targetColumn = ctx.Column + (if ctx.Config.SpaceAroundDelimiter then 3 else 2) - - atCurrentColumn - (genSingleTextNodeSuffixDelimiter node.OpeningBrace - +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> col sepNln node.Fields (fun fieldNode -> - let expr = - if ctx.Config.IndentSize < 3 then - sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth ( - genExpr fieldNode.Expr - ) - else - sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth ( - genExpr fieldNode.Expr - ) - - // 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 - +> atCurrentColumn (enterNode fieldNode +> genSingleTextNode fieldNode.Ident) - +> sepSpace - +> genSingleTextNode fieldNode.Equals - +> expr - +> leaveNode fieldNode) - +> addSpaceIfSpaceAroundDelimiter - +> genSingleTextNode node.ClosingBrace) - ctx - - onlyIf node.IsStruct !- "struct " +> recordExpr - - let genMultilineAnonRecordAlignBrackets = - let genAnonRecord = - match node.CopyInfo with - | Some ci -> - let copyExpr fieldsExpr e = - atCurrentColumnIndent (genExpr e) - +> (!- " with" - +> indent - +> whenShortIndent indent - +> sepNln - +> fieldsExpr - +> whenShortIndent unindent - +> unindent) - - genSingleTextNodeSuffixDelimiter node.OpeningBrace - +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> copyExpr fieldsExpr ci - +> sepNln - +> genSingleTextNode node.ClosingBrace - | None -> - genSingleTextNode node.OpeningBrace - +> indentSepNlnUnindent fieldsExpr - +> sepNln - +> genSingleTextNode node.ClosingBrace - - ifElse node.IsStruct !- "struct " sepNone +> genAnonRecord + ) - ifAlignOrStroustrupBrackets genMultilineAnonRecordAlignBrackets genMultilineAnonRecord + ifAlignOrStroustrupBrackets genMultilineAlignBrackets genMultilineCramped - fun (ctx: Context) -> - let size = getRecordSize ctx node.Fields - genNode node (isSmallExpression size smallExpression longExpression) ctx + genRecord genSmallInheritRecordExpr genMultilineInheritRecordExpr node | Expr.ObjExpr node -> let param = optSingle genExpr node.Expr @@ -835,7 +645,7 @@ let genExpr (e: Expr) = onlyIf (isMultiline && ctx.Config.MultiLineLambdaClosingNewline - && not (ctx.Config.ExperimentalStroustrupStyle && node.Lambda.Expr.IsStroustrupStyleExpr)) + && not (isStroustrupStyleExpr ctx.Config node.Lambda.Expr)) sepNln ctx) +> genSingleTextNode node.ClosingParen @@ -956,7 +766,7 @@ let genExpr (e: Expr) = let genExpr e = match e with | Expr.Record _ - | Expr.AnonRecord _ -> atCurrentColumnIndent (genExpr e) + | Expr.AnonStructRecord _ -> atCurrentColumnIndent (genExpr e) | _ -> genExpr e genExpr node.LeftHandSide @@ -1036,9 +846,7 @@ let genExpr (e: Expr) = +> onlyIfCtx (fun ctx -> ctx.Config.MultiLineLambdaClosingNewline - && (not ( - ctx.Config.ExperimentalStroustrupStyle && lambdaNode.Expr.IsStroustrupStyleExpr - ))) + && (not (isStroustrupStyleExpr ctx.Config lambdaNode.Expr))) sepNln +> genSingleTextNode appParen.Paren.ClosingParen | _ -> @@ -1231,23 +1039,27 @@ let genExpr (e: Expr) = | Expr.App node -> fun ctx -> match node with - | EndsWithDualListApp ctx.Config (sequentialArgs: Expr list, firstList, lastList) -> + | EndsWithDualListApp ctx.Config (sequentialArgs: Expr list, + firstList: ExprArrayOrListNode, + lastList: ExprArrayOrListNode) -> + let genArrayOrList = genArrayOrList false + // check if everything else beside the last array/list fits on one line let singleLineTestExpr = genExpr node.FunctionExpr +> sepSpace +> col sepSpace sequentialArgs genExpr +> sepSpace - +> genExpr firstList + +> genArrayOrList firstList let short = genExpr node.FunctionExpr +> sepSpace +> col sepSpace sequentialArgs genExpr +> onlyIfNot sequentialArgs.IsEmpty sepSpace - +> genExpr firstList + +> genArrayOrList firstList +> sepSpace - +> genExpr lastList + +> genArrayOrList lastList let long = // check if everything besides both lists fits on one line @@ -1260,25 +1072,27 @@ let genExpr (e: Expr) = +> sepNln +> col sepNln sequentialArgs genExpr +> sepSpace - +> genExpr firstList + +> genArrayOrList firstList +> sepSpace - +> genExpr lastList + +> genArrayOrList lastList +> unindent else genExpr node.FunctionExpr +> sepSpace +> col sepSpace sequentialArgs genExpr +> onlyIfNot sequentialArgs.IsEmpty sepSpace - +> genExpr firstList + +> genArrayOrList firstList +> sepSpace - +> genExpr lastList + +> genArrayOrList lastList if futureNlnCheck singleLineTestExpr ctx then long ctx else short ctx - | EndsWithSingleListApp ctx.Config (sequentialArgs: Expr list, arrayOrList) -> + | EndsWithSingleListApp ctx.Config (sequentialArgs: Expr list, arrayOrList: ExprArrayOrListNode) -> + let genArrayOrList = genArrayOrList false + // check if everything else beside the last array/list fits on one line let singleLineTestExpr = genExpr node.FunctionExpr +> sepSpace +> col sepSpace sequentialArgs genExpr @@ -1288,7 +1102,7 @@ let genExpr (e: Expr) = +> sepSpace +> col sepSpace sequentialArgs genExpr +> onlyIfNot sequentialArgs.IsEmpty sepSpace - +> genExpr arrayOrList + +> genArrayOrList arrayOrList let long = genExpr node.FunctionExpr @@ -1296,7 +1110,7 @@ let genExpr (e: Expr) = +> sepNln +> col sepNln sequentialArgs genExpr +> onlyIfNot sequentialArgs.IsEmpty sepNln - +> genExpr arrayOrList + +> genArrayOrList arrayOrList +> unindent if futureNlnCheck singleLineTestExpr ctx then @@ -1304,6 +1118,31 @@ let genExpr (e: Expr) = else short ctx + | EndsWithSingleRecordApp ctx.Config (sequentialArgs: Expr list, recordOrAnonRecord) -> + // check if everything else beside the last array/list fits on one line + let singleLineTestExpr = + genExpr node.FunctionExpr +> sepSpace +> col sepSpace sequentialArgs genExpr + + let short = + genExpr node.FunctionExpr + +> sepSpace + +> col sepSpace sequentialArgs genExpr + +> onlyIfNot sequentialArgs.IsEmpty sepSpace + +> genExpr recordOrAnonRecord + + let long = + genExpr node.FunctionExpr + +> indent + +> sepNln + +> col sepNln sequentialArgs genExpr + +> onlyIfNot sequentialArgs.IsEmpty sepNln + +> genExpr recordOrAnonRecord + +> unindent + + if futureNlnCheck singleLineTestExpr ctx then + long ctx + else + short ctx | _ -> let shortExpression = let sep ctx = @@ -1362,7 +1201,7 @@ let genExpr (e: Expr) = clauseNode.WhenExpr +> sepSpace +> genSingleTextNodeWithSpaceSuffix sepSpace clauseNode.Arrow - +> autoIndentAndNlnExpressUnlessStroustrup genExpr clauseNode.BodyExpr + +> indentSepNlnUnindentUnlessStroustrup genExpr clauseNode.BodyExpr +> leaveNode clauseNode atCurrentColumn ( @@ -1671,7 +1510,7 @@ let genExpr (e: Expr) = |> fun ctx -> { ctx with Config = currentConfig } |> atCurrentColumnIndent - onlyIfCtx (fun ctx -> ctx.Config.StrictMode) (!- "$\"") + onlyIfCtx (fun ctx -> not ctx.HasSource) (!- "$\"") +> col sepNone node.Parts (fun part -> match part with | Choice1Of2 stringNode -> genSingleTextNode stringNode @@ -1681,11 +1520,11 @@ let genExpr (e: Expr) = genInterpolatedFillExpr fillNode.Expr +> optSingle (fun format -> sepColonFixed +> genSingleTextNode format) fillNode.Ident - if ctx.Config.StrictMode then + if not ctx.HasSource then (!- "{" +> genFill +> !- "}") ctx else genFill ctx) - +> onlyIfCtx (fun ctx -> ctx.Config.StrictMode) (!- "\"") + +> onlyIfCtx (fun ctx -> not ctx.HasSource) (!- "\"") |> genNode node | Expr.IndexRangeWildcard node -> genSingleTextNode node | Expr.TripleNumberIndexRange node -> @@ -1711,6 +1550,218 @@ let genQuoteExpr (node: ExprQuoteNode) = +> genSingleTextNode node.CloseToken |> genNode node +/// +/// Prints the inside of an update record expression. +/// This function does not print the opening and closing braces. +/// +/// Should there be an additional indent after the `with` keyword. +/// Record fields. +/// Expression before the `with` keyword. +let genMultilineRecordCopyExpr (addAdditionalIndent: bool) fieldsExpr copyExpr = + atCurrentColumnIndent (genExpr copyExpr) + +> !- " with" + +> indent + +> onlyIf addAdditionalIndent indent + +> sepNln + +> fieldsExpr + +> onlyIf addAdditionalIndent unindent + +> unindent + +let genRecordFieldName (node: RecordFieldNode) = + atCurrentColumn ( + enterNode node + +> genIdentListNode node.FieldName + +> sepSpace + +> genSingleTextNode node.Equals + ) + +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr + +> leaveNode node + +let genMultilineRecordFieldsExpr (node: ExprRecordBaseNode) = + col sepNln node.Fields genRecordFieldName + +/// +/// Print a (anonymous) record with additional information as a single line. +/// +/// Either the `expr with` or `inherit T`. +/// ExprRecordBaseNode +let genSmallRecordBaseExpr genExtra (node: ExprRecordBaseNode) = + genSingleTextNode node.OpeningBrace + +> addSpaceIfSpaceAroundDelimiter + +> genExtra + +> col sepSemi node.Fields genRecordFieldName + +> addSpaceIfSpaceAroundDelimiter + +> genSingleTextNode node.ClosingBrace + +let genSmallRecordNode (node: ExprRecordNode) = + genSmallRecordBaseExpr + (match node.CopyInfo with + | Some we -> genExpr we +> !- " with " + | None -> sepNone) + node + +/// +/// Print a multiline ExprRecordNode. +/// +/// Depending on the actual record, either regular or anonymous, +/// a different strategy needs to be provided to deal with the record fields in the Cramped style. +/// This is too avoid offset errors when using a smaller `indent_size`. +/// +/// +/// The ExprRecordNode +/// Context +let genMultilineRecord (node: ExprRecordNode) (ctx: Context) = + let expressionStartColumn = ctx.Column + let openBraceLength = node.OpeningBrace.Text.Length + + let targetColumn = + expressionStartColumn + + (if ctx.Config.SpaceAroundDelimiter then + openBraceLength + 1 + else + openBraceLength) + + let genMultilineAlignBrackets = + let fieldsExpr = genMultilineRecordFieldsExpr node + + match node.CopyInfo with + | Some ci -> + let additionalIndent = ctx.Config.IndentSize < 3 + + genSingleTextNodeSuffixDelimiter node.OpeningBrace + +> ifElseCtx + (fun ctx -> ctx.Config.IsStroustrupStyle) + (indent +> sepNln) + sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace + +> genMultilineRecordCopyExpr additionalIndent fieldsExpr ci + +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent + +> sepNln + +> genSingleTextNode node.ClosingBrace + | None -> + genSingleTextNode node.OpeningBrace + +> indentSepNlnUnindent fieldsExpr + +> ifElseCtx lastWriteEventIsNewline sepNone sepNln + +> genSingleTextNode node.ClosingBrace + + let genMultilineCramped = + let genFields = + match node.CopyInfo with + | Some we -> + let additionalIndent = + // Anonymous record + (openBraceLength = 2 && ctx.Config.IndentSize <= 3) + // Regular record + || ctx.Config.IndentSize < 3 + + genMultilineRecordCopyExpr additionalIndent (genMultilineRecordFieldsExpr node) we + | None -> + fun (ctx: Context) -> + col + sepNln + node.Fields + (fun e -> + // Add spaces to ensure the record field (incl trivia) starts at the right column. + addFixedSpaces targetColumn + // Potential indentations will be in relation to the opening curly brace. + +> genRecordFieldName e) + ctx + + match node.CopyInfo with + | Some _ -> + genSingleTextNode node.OpeningBrace + +> sepNlnWhenWriteBeforeNewlineNotEmptyOr addSpaceIfSpaceAroundDelimiter // comment after curly brace + +> genFields + +> addSpaceIfSpaceAroundDelimiter + +> genSingleTextNode node.ClosingBrace + | None -> + atCurrentColumn ( + genSingleTextNodeSuffixDelimiter node.OpeningBrace + +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace + +> genFields + +> sepNlnWhenWriteBeforeNewlineNotEmpty + +> (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 brace = + addFixedSpaces expressionStartColumn +> genSingleTextNode node.ClosingBrace + + ifElseCtx lastWriteEventIsNewline brace (addSpaceIfSpaceAroundDelimiter +> brace) ctx) + ) + + ifAlignOrStroustrupBrackets genMultilineAlignBrackets genMultilineCramped ctx + +let genRecord smallRecordExpr multilineRecordExpr (node: ExprRecordBaseNode) ctx = + let size = getRecordSize ctx node.Fields + genNode node (isSmallExpression size smallRecordExpr multilineRecordExpr) ctx + +let genArrayOrList (preferMultilineCramped: bool) (node: ExprArrayOrListNode) = + if node.Elements.IsEmpty then + genSingleTextNode node.Opening +> genSingleTextNode node.Closing |> genNode node + else + let smallExpression = + genSingleTextNode node.Opening + +> addSpaceIfSpaceAroundDelimiter + +> col sepSemi node.Elements genExpr + +> addSpaceIfSpaceAroundDelimiter + +> genSingleTextNode node.Closing + + let multilineExpression = + let genMultiLineArrayOrListAlignBrackets = + genSingleTextNode node.Opening + +> indent + +> sepNlnUnlessLastEventIsNewline + +> col sepNln node.Elements genExpr + +> unindent + +> sepNlnUnlessLastEventIsNewline + +> genSingleTextNode node.Closing + + let genMultiLineArrayOrListCramped = + genSingleTextNodeSuffixDelimiter node.Opening + +> atCurrentColumnIndent ( + sepNlnWhenWriteBeforeNewlineNotEmpty + +> col sepNln node.Elements genExpr + +> (enterNode node.Closing + +> (fun ctx -> + let isFixed = lastWriteEventIsNewline ctx + (onlyIfNot isFixed sepSpace +> !-node.Closing.Text +> leaveNode node.Closing) ctx)) + ) + + ifElse preferMultilineCramped genMultiLineArrayOrListCramped genMultiLineArrayOrListAlignBrackets + + fun ctx -> + let alwaysMultiline = + let isIfThenElseWithYieldReturn e = + let (|YieldLikeExpr|_|) e = + match e with + | Expr.Single singleNode -> + if singleNode.Leading.Text.StartsWith("yield") then + Some e + else + None + | _ -> None + + match e with + | Expr.IfThen ifThenNode -> + match ifThenNode.ThenExpr with + | YieldLikeExpr _ -> true + | _ -> false + | Expr.IfThenElse ifThenElseNode -> + match ifThenElseNode.IfExpr, ifThenElseNode.ElseExpr with + | YieldLikeExpr _, _ + | _, YieldLikeExpr _ -> true + | _ -> false + | _ -> false + + List.exists isIfThenElseWithYieldReturn node.Elements + || List.forall isSynExprLambdaOrIfThenElse node.Elements + + if alwaysMultiline then + multilineExpression ctx + else + let size = getListOrArrayExprSize ctx ctx.Config.MaxArrayOrListWidth node.Elements + isSmallExpression size smallExpression multilineExpression ctx + |> genNode node + let genMultilineFunctionApplicationArguments (argExpr: Expr) = let argsInsideParenthesis (parenNode: ExprParenNode) f = genSingleTextNode parenNode.OpeningParen @@ -1804,7 +1855,7 @@ let genNamedArgumentExpr (node: ExprInfixAppNode) = genExpr node.LeftHandSide +> sepSpace +> genSingleTextNode node.Operator - +> autoIndentAndNlnExpressUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide + +> indentSepNlnUnindentUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide expressionFitsOnRestOfLine short long |> genNode node @@ -1902,7 +1953,7 @@ let genClause (isLastItem: bool) (node: MatchClauseNode) = ctx) let genPatAndBody ctx = - if ctx.Config.ExperimentalStroustrupStyle && node.BodyExpr.IsStroustrupStyleExpr then + if isStroustrupStyleExpr ctx.Config node.BodyExpr then let startColumn = ctx.Column (genPatInClause node.Pattern +> atIndentLevel false startColumn genWhenAndBody) ctx else @@ -2104,7 +2155,7 @@ let genFunctionNameWithMultilineLids (trailing: Context -> Context) (longIdent: |> genNode parentNode let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.ExperimentalStroustrupStyle then + if not (config.ExperimentalElmish || config.IsStroustrupStyle) then None else let mutable otherArgs = ListCollector() @@ -2112,8 +2163,7 @@ let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = let rec visit (args: Expr list) = match args with | [] -> None - | [ Expr.ArrayOrList _ as firstList; Expr.ArrayOrList _ as lastList ] -> - Some(otherArgs.Close(), firstList, lastList) + | [ Expr.ArrayOrList firstList; Expr.ArrayOrList lastList ] -> Some(otherArgs.Close(), firstList, lastList) | arg :: args -> otherArgs.Add(arg) visit args @@ -2121,7 +2171,7 @@ let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = visit appNode.Arguments let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.ExperimentalStroustrupStyle then + if not (config.ExperimentalElmish || config.IsStroustrupStyle) then None else let mutable otherArgs = ListCollector() @@ -2129,7 +2179,23 @@ let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = let rec visit (args: Expr list) = match args with | [] -> None - | [ Expr.ArrayOrList _ as singleList ] -> Some(otherArgs.Close(), singleList) + | [ Expr.ArrayOrList singleList ] -> Some(otherArgs.Close(), singleList) + | arg :: args -> + otherArgs.Add(arg) + visit args + + visit appNode.Arguments + +let (|EndsWithSingleRecordApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = + if not config.IsStroustrupStyle then + None + else + let mutable otherArgs = ListCollector() + + let rec visit (args: Expr list) = + match args with + | [] -> None + | [ Expr.Record _ | Expr.AnonStructRecord _ as singleRecord ] -> Some(otherArgs.Close(), singleRecord) | arg :: args -> otherArgs.Add(arg) visit args @@ -2161,9 +2227,7 @@ let genAppWithLambda sep (node: ExprAppWithLambdaNode) = | Choice1Of2 lambdaNode -> genSingleTextNode node.OpeningParen +> (genLambdaWithParen lambdaNode |> genNode lambdaNode) - +> onlyIf - (not (ctx.Config.ExperimentalStroustrupStyle && lambdaNode.Expr.IsStroustrupStyleExpr)) - sepNln + +> onlyIf (not (isStroustrupStyleExpr ctx.Config lambdaNode.Expr)) sepNln +> genSingleTextNode node.ClosingParen | Choice2Of2 matchLambdaNode -> genSingleTextNode node.OpeningParen @@ -2185,11 +2249,7 @@ let genAppWithLambda sep (node: ExprAppWithLambdaNode) = +> (genLambdaWithParen lambdaNode |> genNode lambdaNode)) (fun isMultiline -> onlyIf - (isMultiline - && not ( - ctx.Config.ExperimentalStroustrupStyle - && lambdaNode.Expr.IsStroustrupStyleExpr - )) + (isMultiline && not (isStroustrupStyleExpr ctx.Config lambdaNode.Expr)) sepNln +> genSingleTextNode node.ClosingParen) | Choice2Of2 matchLambdaNode -> @@ -2671,7 +2731,7 @@ let genBinding (b: BindingNode) (ctx: Context) : Context = let short = sepSpace +> body let long = - autoIndentAndNlnExpressUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr + indentSepNlnUnindentUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr isShortExpression ctx.Config.MaxFunctionBindingWidth short long @@ -2760,7 +2820,7 @@ let genBinding (b: BindingNode) (ctx: Context) : Context = +> (fun ctx -> let prefix = afterLetKeyword +> sepSpace +> genValueName +> genReturnType let short = prefix +> genExpr b.Expr - let long = prefix +> autoIndentAndNlnExpressUnlessStroustrup genExpr b.Expr + let long = prefix +> indentSepNlnUnindentUnlessStroustrup genExpr b.Expr isShortExpression ctx.Config.MaxValueBindingWidth short long ctx) genNode b binding ctx @@ -3009,7 +3069,7 @@ let addSpaceIfSynTypeStaticConstantHasAtSignBeforeString (t: Type) = | _ -> sepNone | _ -> sepNone -let sepNlnTypeAndMembers (node: ITypeDefn) (ctx: Context) : Context = +let sepNlnBetweenTypeAndMembers (node: ITypeDefn) (ctx: Context) : Context = match node.Members with | [] -> sepNone ctx | firstMember :: _ -> @@ -3138,7 +3198,7 @@ let genTypeDefn (td: TypeDefn) = +> indentSepNlnUnindent ( col sepNln node.EnumCases genEnumCase +> onlyIf hasMembers sepNln - +> sepNlnTypeAndMembers typeDefnNode + +> sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members ) |> genNode node @@ -3171,11 +3231,12 @@ let genTypeDefn (td: TypeDefn) = header +> unionCases - +> onlyIf hasMembers (indentSepNlnUnindent (sepNlnTypeAndMembers typeDefnNode +> genMemberDefnList members)) + +> onlyIf + hasMembers + (indentSepNlnUnindent (sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members)) |> genNode node | TypeDefn.Record node -> let hasMembers = List.isNotEmpty members - let hasNoMembers = not hasMembers let multilineExpression (ctx: Context) = let genRecordFields = @@ -3185,9 +3246,7 @@ let genTypeDefn (td: TypeDefn) = +> genSingleTextNode node.ClosingBrace let genMembers = - onlyIf hasMembers sepNln - +> sepNlnTypeAndMembers typeDefnNode - +> genMemberDefnList members + onlyIf hasMembers (sepNln +> sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members) let anyFieldHasXmlDoc = List.exists (fun (fieldNode: FieldNode) -> fieldNode.XmlDoc.IsSome) node.Fields @@ -3198,8 +3257,15 @@ let genTypeDefn (td: TypeDefn) = +> optSingle (fun _ -> unindent) node.Accessibility +> genMembers - let stroustrupWithoutMembers = - genAccessOpt node.Accessibility +> genRecordFields +> genMembers + let stroustrup = + let withKw = + match typeName.WithKeyword with + | None -> !- "with" + | Some withNode -> genSingleTextNode withNode + + genAccessOpt node.Accessibility + +> genRecordFields + +> onlyIf hasMembers (sepSpace +> withKw +> indent +> genMembers +> unindent) let cramped = sepNlnUnlessLastEventIsNewline @@ -3209,19 +3275,18 @@ let genTypeDefn (td: TypeDefn) = +> addSpaceIfSpaceAroundDelimiter +> genSingleTextNode node.ClosingBrace +> optSingle (fun _ -> unindent) node.Accessibility - +> onlyIf hasMembers sepNln - +> sepNlnTypeAndMembers typeDefnNode - +> genMemberDefnList members + +> genMembers match ctx.Config.MultilineBracketStyle with - | ExperimentalStroustrup when hasNoMembers -> stroustrupWithoutMembers ctx - | Aligned - | ExperimentalStroustrup -> aligned ctx + | Stroustrup -> stroustrup ctx + | Aligned -> aligned ctx | Cramped when anyFieldHasXmlDoc -> aligned ctx | Cramped -> cramped ctx let bodyExpr size = - if hasNoMembers then + if hasMembers then + multilineExpression + else let smallExpression = sepSpace +> genAccessOpt node.Accessibility @@ -3233,14 +3298,12 @@ let genTypeDefn (td: TypeDefn) = +> genSingleTextNode node.ClosingBrace isSmallExpression size smallExpression multilineExpression - else - multilineExpression let genTypeDefinition (ctx: Context) = let size = getRecordSize ctx node.Fields let short = bodyExpr size - if ctx.Config.ExperimentalStroustrupStyle && hasNoMembers then + if ctx.Config.IsStroustrupStyle then (sepSpace +> short) ctx else isSmallExpression size short (indentSepNlnUnindent short) ctx @@ -3252,7 +3315,7 @@ let genTypeDefn (td: TypeDefn) = fun (ctx: Context) -> (match node.Type with - | Type.AnonRecord _ when not hasMembers && ctx.Config.ExperimentalStroustrupStyle -> + | Type.AnonRecord _ when not hasMembers && ctx.Config.IsStroustrupStyle -> header +> sepSpaceOrIndentAndNlnIfTypeExceedsPageWidthUnlessStroustrup genType node.Type |> genNode node @@ -3295,7 +3358,7 @@ let genTypeDefn (td: TypeDefn) = header +> sepSpace +> optSingle genSingleTextNode typeName.WithKeyword - +> indentSepNlnUnindent (sepNlnTypeAndMembers typeDefnNode +> genMemberDefnList members) + +> indentSepNlnUnindent (sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members) |> genNode node | TypeDefn.Delegate node -> header diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 8ddb9a2231..c7876b4907 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -1,8 +1,8 @@ module internal Fantomas.Core.Context open System +open FSharp.Compiler.Text open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas.Core.SyntaxOak type WriterEvent = @@ -179,19 +179,25 @@ module WriterEvents = | _ -> false) [] -type internal Context = +type Context = { Config: FormatConfig + HasSource: bool WriterModel: WriterModel - WriterEvents: Queue } + WriterEvents: Queue + FormattedCursor: pos option } /// Initialize with a string writer and use space as delimiter static member Default = { Config = FormatConfig.Default + HasSource = false WriterModel = WriterModel.init - WriterEvents = Queue.empty } + WriterEvents = Queue.empty + FormattedCursor = None } - static member Create config : Context = - { Context.Default with Config = config } + static member Create hasSource config : Context = + { Context.Default with + Config = config + HasSource = hasSource } member x.WithDummy(writerCommands, ?keepPageWidth) = let keepPageWidth = keepPageWidth |> Option.defaultValue false @@ -267,16 +273,20 @@ let finalizeWriterModel (ctx: Context) = let dump (isSelection: bool) (ctx: Context) = let ctx = finalizeWriterModel ctx - match ctx.WriterModel.Lines with - | [] -> [] - | h :: tail -> - // Always trim the last line - h.TrimEnd() :: tail - |> List.rev - |> fun lines -> - // Don't skip leading newlines when formatting a selection. - if isSelection then lines else List.skipWhile ((=) "") lines - |> String.concat ctx.Config.EndOfLine.NewLineString + let code = + match ctx.WriterModel.Lines with + | [] -> [] + | h :: tail -> + // Always trim the last line + h.TrimEnd() :: tail + |> List.rev + |> fun lines -> + // Don't skip leading newlines when formatting a selection. + if isSelection then lines else List.skipWhile ((=) "") lines + |> String.concat ctx.Config.EndOfLine.NewLineString + + { Code = code + Cursor = ctx.FormattedCursor } let dumpAndContinue (ctx: Context) = #if DEBUG @@ -746,21 +756,44 @@ let sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth expr (ctx: Context) expr ctx +let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = + let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup + + match e with + | Expr.Record _ + | Expr.AnonStructRecord _ + | Expr.ArrayOrList _ -> isStroustrupEnabled + | Expr.NamedComputation _ -> not config.NewlineBeforeMultilineComputationExpression + | _ -> false + +let isStroustrupStyleType (config: FormatConfig) (t: Type) = + let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup + + match t with + | Type.AnonRecord _ when isStroustrupEnabled -> true + | _ -> false + +let canSafelyUseStroustrup (node: Node) = not node.HasContentBefore + let sepSpaceOrIndentAndNlnIfExceedsPageWidthUnlessStroustrup isStroustrup f (node: Node) (ctx: Context) = - if - ctx.Config.ExperimentalStroustrupStyle - && isStroustrup - && Seq.isEmpty node.ContentBefore - then + if isStroustrup && canSafelyUseStroustrup node then (sepSpace +> f) ctx else sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth f ctx -let sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup f (expr: Expr) = - sepSpaceOrIndentAndNlnIfExceedsPageWidthUnlessStroustrup expr.IsStroustrupStyleExpr (f expr) (Expr.Node expr) +let sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup f (expr: Expr) (ctx: Context) = + sepSpaceOrIndentAndNlnIfExceedsPageWidthUnlessStroustrup + (isStroustrupStyleExpr ctx.Config expr) + (f expr) + (Expr.Node expr) + ctx -let sepSpaceOrIndentAndNlnIfTypeExceedsPageWidthUnlessStroustrup f (t: Type) = - sepSpaceOrIndentAndNlnIfExceedsPageWidthUnlessStroustrup t.IsStroustrupStyleType (f t) (Type.Node t) +let sepSpaceOrIndentAndNlnIfTypeExceedsPageWidthUnlessStroustrup f (t: Type) (ctx: Context) = + sepSpaceOrIndentAndNlnIfExceedsPageWidthUnlessStroustrup + (isStroustrupStyleType ctx.Config t) + (f t) + (Type.Node t) + ctx let autoNlnIfExpressionExceedsPageWidth expr (ctx: Context) = expressionExceedsPageWidth @@ -860,7 +893,7 @@ let ifAlignOrStroustrupBrackets f g = (fun ctx -> match ctx.Config.MultilineBracketStyle with | Aligned - | ExperimentalStroustrup -> true + | Stroustrup -> true | Cramped -> false) f g @@ -899,53 +932,41 @@ let addParenIfAutoNln expr f = let expr = f expr expressionFitsOnRestOfLine expr (ifElse hasParenthesis (sepOpenT +> expr +> sepCloseT) expr) -let autoIndentAndNlnExpressUnlessStroustrup (f: Expr -> Context -> Context) (e: Expr) (ctx: Context) = +let indentSepNlnUnindentUnlessStroustrup f (e: Expr) (ctx: Context) = let shouldUseStroustrup = - ctx.Config.ExperimentalStroustrupStyle - && e.IsStroustrupStyleExpr - && let node = Expr.Node e in - Seq.isEmpty node.ContentBefore + isStroustrupStyleExpr ctx.Config e && canSafelyUseStroustrup (Expr.Node e) if shouldUseStroustrup then f e ctx else indentSepNlnUnindent (f e) ctx -let autoIndentAndNlnTypeUnlessStroustrup (f: Type -> Context -> Context) (t: Type) (ctx: Context) = +let autoIndentAndNlnTypeUnlessStroustrup f (t: Type) (ctx: Context) = let shouldUseStroustrup = - ctx.Config.ExperimentalStroustrupStyle - && t.IsStroustrupStyleType - && let node = Type.Node t in - Seq.isEmpty node.ContentBefore + isStroustrupStyleType ctx.Config t && canSafelyUseStroustrup (Type.Node t) if shouldUseStroustrup then f t ctx else autoIndentAndNlnIfExpressionExceedsPageWidth (f t) ctx -let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup - (f: Expr -> Context -> Context) - (e: Expr) - (ctx: Context) - = +let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup f (e: Expr) (ctx: Context) = let isStroustrup = - ctx.Config.ExperimentalStroustrupStyle - && e.IsStroustrupStyleExpr - && Seq.isEmpty (Expr.Node e).ContentBefore + isStroustrupStyleExpr ctx.Config e && canSafelyUseStroustrup (Expr.Node e) if isStroustrup then f e ctx else autoIndentAndNlnIfExpressionExceedsPageWidth (f e) ctx -type internal ColMultilineItem = +type ColMultilineItem = | ColMultilineItem of // current expression expr: (Context -> Context) * // sepNln of current item sepNln: (Context -> Context) -type internal ColMultilineItemsState = +type ColMultilineItemsState = { LastBlockMultiline: bool Context: Context } diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index cf11bd354d..d54c6938c6 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -1,7 +1,6 @@ module internal Fantomas.Core.Context -open Fantomas.Core -open Fantomas.Core.FormatConfig +open FSharp.Compiler.Text open Fantomas.Core.SyntaxOak type WriterEvent = @@ -53,14 +52,20 @@ type WriterModel = member IsDummy: bool [] -type internal Context = - { Config: FormatConfig - WriterModel: WriterModel - WriterEvents: Queue } +type Context = + { + Config: FormatConfig + /// Indicates the presence of source code. + /// This could be absent in the case we are formatting from AST. + HasSource: bool + WriterModel: WriterModel + WriterEvents: Queue + FormattedCursor: pos option + } /// Initialize with a string writer and use space as delimiter static member Default: Context - static member Create: config: FormatConfig -> Context + static member Create: hasSource: bool -> config: FormatConfig -> Context member WithDummy: writerCommands: Queue * ?keepPageWidth: bool -> Context member WithShortExpression: maxWidth: int * ?startColumn: int -> Context member Column: int @@ -70,8 +75,7 @@ type internal Context = /// The event is also being processed in the WriterModel of the Context. val writerEvent: e: WriterEvent -> ctx: Context -> Context val hasWriteBeforeNewlineContent: ctx: Context -> bool -// val finalizeWriterModel: ctx: Context -> Context -val dump: isSelection: bool -> ctx: Context -> string +val dump: isSelection: bool -> ctx: Context -> FormatResult val dumpAndContinue: ctx: Context -> Context val lastWriteEventIsNewline: ctx: Context -> bool @@ -235,6 +239,8 @@ val sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup: val sepSpaceOrIndentAndNlnIfTypeExceedsPageWidthUnlessStroustrup: f: (Type -> Context -> Context) -> t: Type -> (Context -> Context) +val isStroustrupStyleExpr: config: FormatConfig -> e: Expr -> bool + val autoParenthesisIfExpressionExceedsPageWidth: expr: (Context -> Context) -> ctx: Context -> Context val futureNlnCheck: f: (Context -> Context) -> ctx: Context -> bool /// similar to futureNlnCheck but validates whether the expression is going over the max page width @@ -255,13 +261,15 @@ val sepNlnWhenWriteBeforeNewlineNotEmpty: (Context -> Context) val sepSpaceUnlessWriteBeforeNewlineNotEmpty: ctx: Context -> Context val autoIndentAndNlnWhenWriteBeforeNewlineNotEmpty: f: (Context -> Context) -> ctx: Context -> Context val addParenIfAutoNln: expr: Expr -> f: (Expr -> Context -> Context) -> (Context -> Context) -val autoIndentAndNlnExpressUnlessStroustrup: f: (Expr -> Context -> Context) -> e: Expr -> ctx: Context -> Context + +val indentSepNlnUnindentUnlessStroustrup: f: (Expr -> Context -> Context) -> e: Expr -> ctx: Context -> Context + val autoIndentAndNlnTypeUnlessStroustrup: f: (Type -> Context -> Context) -> t: Type -> ctx: Context -> Context val autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup: f: (Expr -> Context -> Context) -> e: Expr -> ctx: Context -> Context -type internal ColMultilineItem = ColMultilineItem of expr: (Context -> Context) * sepNln: (Context -> Context) +type ColMultilineItem = ColMultilineItem of expr: (Context -> Context) * sepNln: (Context -> Context) /// This helper function takes a list of expressions and ranges. /// If the expression is multiline it will add a newline before and after the expression. diff --git a/src/Fantomas.Core/Defines.fs b/src/Fantomas.Core/Defines.fs index 79765862b4..a80f0900bf 100644 --- a/src/Fantomas.Core/Defines.fs +++ b/src/Fantomas.Core/Defines.fs @@ -1,8 +1,16 @@ -module internal Fantomas.Core.Defines +namespace Fantomas.Core open FSharp.Compiler.SyntaxTrivia open Fantomas.Core -open Fantomas.Core.SyntaxOak + +type internal DefineCombination = + | DefineCombination of defines: string list + + member x.Value = + match x with + | DefineCombination defines -> defines + + static member Empty = DefineCombination([]) module private DefineCombinationSolver = let rec map f e = @@ -213,54 +221,57 @@ module private DefineCombinationSolver = r -let getIndividualDefine (hashDirectives: ConditionalDirectiveTrivia list) : string list list = - let rec visit (expr: IfDirectiveExpression) : string list = - match expr with - | IfDirectiveExpression.Not expr -> visit expr - | IfDirectiveExpression.And(e1, e2) - | IfDirectiveExpression.Or(e1, e2) -> visit e1 @ visit e2 - | IfDirectiveExpression.Ident s -> List.singleton s - - hashDirectives - |> List.collect (function - | ConditionalDirectiveTrivia.If(expr, _r) -> visit expr - | _ -> []) - |> List.distinct - |> List.map List.singleton - -let getDefineExprs (hashDirectives: ConditionalDirectiveTrivia list) = - let result = - (([], []), hashDirectives) - ||> List.fold (fun (contextExprs, exprAcc) hashLine -> - let contextExpr e = - e :: contextExprs |> List.reduce (fun x y -> IfDirectiveExpression.And(x, y)) - - match hashLine with - | ConditionalDirectiveTrivia.If(expr, _) -> expr :: contextExprs, contextExpr expr :: exprAcc - | ConditionalDirectiveTrivia.Else _ -> - contextExprs, - IfDirectiveExpression.Not(contextExprs |> List.reduce (fun x y -> IfDirectiveExpression.And(x, y))) - :: exprAcc - | ConditionalDirectiveTrivia.EndIf _ -> List.tail contextExprs, exprAcc) - |> snd - |> List.rev - - result - -let satSolveMaxStepsMaxSteps = 100 - -let getOptimizedDefinesSets (hashDirectives: ConditionalDirectiveTrivia list) = - let defineExprs = getDefineExprs hashDirectives - - match - DefineCombinationSolver.mergeBoolExprs satSolveMaxStepsMaxSteps defineExprs - |> List.map snd - with - | [] -> [ [] ] - | xs -> xs - -let getDefineCombination (hashDirectives: ConditionalDirectiveTrivia list) : DefineCombination list = - [ yield [] // always include the empty defines set - yield! getOptimizedDefinesSets hashDirectives - yield! getIndividualDefine hashDirectives ] - |> List.distinct +module Defines = + + let getIndividualDefine (hashDirectives: ConditionalDirectiveTrivia list) : string list list = + let rec visit (expr: IfDirectiveExpression) : string list = + match expr with + | IfDirectiveExpression.Not expr -> visit expr + | IfDirectiveExpression.And(e1, e2) + | IfDirectiveExpression.Or(e1, e2) -> visit e1 @ visit e2 + | IfDirectiveExpression.Ident s -> List.singleton s + + hashDirectives + |> List.collect (function + | ConditionalDirectiveTrivia.If(expr, _r) -> visit expr + | _ -> []) + |> List.distinct + |> List.map List.singleton + + let getDefineExprs (hashDirectives: ConditionalDirectiveTrivia list) = + let result = + (([], []), hashDirectives) + ||> List.fold (fun (contextExprs, exprAcc) hashLine -> + let contextExpr e = + e :: contextExprs |> List.reduce (fun x y -> IfDirectiveExpression.And(x, y)) + + match hashLine with + | ConditionalDirectiveTrivia.If(expr, _) -> expr :: contextExprs, contextExpr expr :: exprAcc + | ConditionalDirectiveTrivia.Else _ -> + contextExprs, + IfDirectiveExpression.Not(contextExprs |> List.reduce (fun x y -> IfDirectiveExpression.And(x, y))) + :: exprAcc + | ConditionalDirectiveTrivia.EndIf _ -> List.tail contextExprs, exprAcc) + |> snd + |> List.rev + + result + + let satSolveMaxStepsMaxSteps = 100 + + let getOptimizedDefinesSets (hashDirectives: ConditionalDirectiveTrivia list) = + let defineExprs = getDefineExprs hashDirectives + + match + DefineCombinationSolver.mergeBoolExprs satSolveMaxStepsMaxSteps defineExprs + |> List.map snd + with + | [] -> [ [] ] + | xs -> xs + + let getDefineCombination (hashDirectives: ConditionalDirectiveTrivia list) : DefineCombination list = + [ yield [] // always include the empty defines set + yield! getOptimizedDefinesSets hashDirectives + yield! getIndividualDefine hashDirectives ] + |> List.distinct + |> List.map DefineCombination diff --git a/src/Fantomas.Core/Defines.fsi b/src/Fantomas.Core/Defines.fsi index 8ce3d5546f..e3d2381c0e 100644 --- a/src/Fantomas.Core/Defines.fsi +++ b/src/Fantomas.Core/Defines.fsi @@ -1,6 +1,13 @@ -module internal Fantomas.Core.Defines +namespace Fantomas.Core -open FSharp.Compiler.SyntaxTrivia -open Fantomas.Core.SyntaxOak +type internal DefineCombination = + | DefineCombination of defines: string list -val getDefineCombination: hashDirectives: ConditionalDirectiveTrivia list -> DefineCombination list + member Value: string list + + static member Empty: DefineCombination + +module internal Defines = + open FSharp.Compiler.SyntaxTrivia + + val getDefineCombination: hashDirectives: ConditionalDirectiveTrivia list -> DefineCombination list diff --git a/src/Fantomas.Core/Fantomas.Core.fsproj b/src/Fantomas.Core/Fantomas.Core.fsproj index f7793f83b1..35aa74aaed 100644 --- a/src/Fantomas.Core/Fantomas.Core.fsproj +++ b/src/Fantomas.Core/Fantomas.Core.fsproj @@ -25,10 +25,13 @@ + + + diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index e46e4b57d5..e2abad7247 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -1,7 +1,10 @@ -module Fantomas.Core.FormatConfig +namespace Fantomas.Core open System open System.ComponentModel +open Fantomas.FCS.Parse + +exception ParseException of diagnostics: FSharpParserDiagnostic list type FormatException(msg: string) = inherit Exception(msg) @@ -26,19 +29,19 @@ type MultilineFormatterType = type MultilineBracketStyle = | Cramped | Aligned - | ExperimentalStroustrup + | Stroustrup static member ToConfigString(cfg: MultilineBracketStyle) = match cfg with | Cramped -> "cramped" | Aligned -> "aligned" - | ExperimentalStroustrup -> "experimental_stroustrup" + | Stroustrup -> "stroustrup" static member OfConfigString(cfgString: string) = match cfgString with | "cramped" -> Some Cramped | "aligned" -> Some Aligned - | "experimental_stroustrup" -> Some ExperimentalStroustrup + | "stroustrup" -> Some Stroustrup | _ -> None [] @@ -210,8 +213,8 @@ type FormatConfig = BarBeforeDiscriminatedUnionDeclaration: bool [] - [] - [] + [] + [] MultilineBracketStyle: MultilineBracketStyle [] @@ -219,11 +222,14 @@ type FormatConfig = KeepMaxNumberOfBlankLines: Num [] - [] - [] - StrictMode: bool } + [] + NewlineBeforeMultilineComputationExpression: bool + + [] + [] + ExperimentalElmish: bool } - member x.ExperimentalStroustrupStyle = x.MultilineBracketStyle = ExperimentalStroustrup + member x.IsStroustrupStyle = x.MultilineBracketStyle = Stroustrup static member Default = { IndentSize = 4 @@ -261,4 +267,5 @@ type FormatConfig = BarBeforeDiscriminatedUnionDeclaration = false MultilineBracketStyle = Cramped KeepMaxNumberOfBlankLines = 100 - StrictMode = false } + NewlineBeforeMultilineComputationExpression = true + ExperimentalElmish = false } diff --git a/src/Fantomas.Core/MultipleDefineCombinations.fs b/src/Fantomas.Core/MultipleDefineCombinations.fs new file mode 100644 index 0000000000..9f3133ca07 --- /dev/null +++ b/src/Fantomas.Core/MultipleDefineCombinations.fs @@ -0,0 +1,297 @@ +module internal Fantomas.Core.MultipleDefineCombinations + +open System +open System.Linq +open System.Text +open System.Text.RegularExpressions +open Microsoft.FSharp.Core.CompilerServices +open FSharp.Compiler.Text + +/// A CodeFragment represents a chunk of code that is either +/// a single conditional hash directive line, +/// non existing content (for a specific combination of defines) or +/// active content. +/// +/// When the code needs to be compared, a CustomComparison is used to determine which fragment we are interested in. +[] +[] +type CodeFragment = + /// Any line that starts with `#if`, `#else` or `#endif` + | HashLine of line: string * defines: DefineCombination + /// Content found between two HashLines + | Content of code: string * lineCount: int * defines: DefineCombination + /// When two HashLines follow each other without any content in between. + | NoContent of defines: DefineCombination + + member x.Defines = + match x with + | HashLine(defines = defines) + | Content(defines = defines) + | NoContent(defines = defines) -> defines + + member x.LineCount = + match x with + | HashLine _ -> 1 + | Content(lineCount = lineCount) -> lineCount + | NoContent _ -> 0 + + override x.Equals(other: obj) = + match other with + | :? CodeFragment as other -> + match x, other with + | HashLine _ as x, (HashLine _ as other) -> x = other + | Content _ as x, (Content _ as other) -> x = other + | NoContent _ as x, (NoContent _ as other) -> x = other + | _ -> false + | _ -> false + + override x.GetHashCode() = + match x with + | HashLine(line, defineCombination) -> + {| line = line + defineCombination = defineCombination |} + .GetHashCode() + | Content(code, lineCount, defines) -> + {| code = code + lineCount = lineCount + defines = defines |} + .GetHashCode() + | NoContent defines -> {| defines = defines |}.GetHashCode() + + interface IComparable with + member x.CompareTo other = + match other with + | :? CodeFragment as other -> (x :> IComparable<_>).CompareTo other + | _ -> -1 + + interface IComparable with + member x.CompareTo y = + match x, y with + // When comparing the different results of each format result, the single constant is that all hash lines + // should exactly match. + | CodeFragment.HashLine(line = lineX), CodeFragment.HashLine(line = lineY) -> + assert (lineX = lineY) + 0 + // Pick the other fragment if it has content you don't + | CodeFragment.NoContent _, CodeFragment.Content _ -> -1 + // Pick our fragment if the other fragment has no code + | CodeFragment.Content _, CodeFragment.NoContent _ -> 1 + // If both fragments are empty they are equivalent. + // Keep in mind that we could be comparing more than the two fragments at the same time in `traverseFragments` + | CodeFragment.NoContent _, CodeFragment.NoContent _ -> 0 + // If both fragments have content, we want to take the content with the most lines. + | CodeFragment.Content(lineCount = ownLineCount; code = ownContent), + CodeFragment.Content(lineCount = otherLineCount; code = otherContent) -> + let hasOwnContent = not (String.IsNullOrWhiteSpace ownContent) + let hasOtherContent = not (String.IsNullOrWhiteSpace otherContent) + + if ownLineCount > otherLineCount then + 1 + elif ownLineCount < otherLineCount then + -1 + elif hasOwnContent && not hasOtherContent then + 1 + elif not hasOwnContent && hasOtherContent then + -1 + else + // This only really tailors to #1026 + // The shortest content is chosen because it will lead to the branch with less indentation. + // This is again very specific to that exact unit test. + let ownLength = ownContent.Length + let otherLength = otherContent.Length + + if ownLength < otherLength then 1 + elif ownLength > otherLength then -1 + else 0 + // This is an unexpected situation. + // You should never enter the case where you need to compare a hash line with something other than a hash line. + | x, other -> failwith $"Cannot compare %A{x} with %A{other}" + +type FormatResultForDefines = + { Result: FormatResult + Defines: DefineCombination + Fragments: CodeFragment list } + +/// Accumulator type used when building up the fragments. +type SplitHashState = + { CurrentBuilder: StringBuilder + LinesCollected: int + LastLineInfo: LastLineInfo } + + static member Zero = + { CurrentBuilder = StringBuilder() + LastLineInfo = LastLineInfo.None + LinesCollected = 0 } + +and [] LastLineInfo = + | None + | HashLine + | Content + +/// Accumulator type used when folding over the selected CodeFragments. +type FragmentWeaverState = + { LastLine: int + Cursors: Map + ContentBuilder: StringBuilder + FoundCursor: (DefineCombination * pos) option } + +let stringBuilderResult (builder: StringBuilder) = builder.ToString() + +let hashRegex = @"^\s*#(if|elseif|else|endif).*" + +/// Split the given `source` into the matching `CodeFragments`. +let splitWhenHash (defines: DefineCombination) (newline: string) (source: string) : CodeFragment list = + let lines = source.Split([| newline |], options = StringSplitOptions.None) + let mutable fragmentsBuilder = ListCollector() + + let closeState (acc: SplitHashState) = + if acc.LastLineInfo = LastLineInfo.Content then + let lastFragment = acc.CurrentBuilder.ToString() + // The last fragment could be a newline after the last #endif + fragmentsBuilder.Add(CodeFragment.Content(lastFragment, acc.LinesCollected, defines)) + + (SplitHashState.Zero, lines) + ||> Array.fold (fun acc line -> + if Regex.IsMatch(line, hashRegex) then + // Only add the previous fragment if it had content + match acc.LastLineInfo with + | LastLineInfo.None -> () + | LastLineInfo.HashLine -> fragmentsBuilder.Add(CodeFragment.NoContent defines) + | LastLineInfo.Content -> + // Close the previous fragment builder + let lastFragment = acc.CurrentBuilder.ToString() + fragmentsBuilder.Add(CodeFragment.Content(lastFragment, acc.LinesCollected, defines)) + + // Add the hashLine + fragmentsBuilder.Add(CodeFragment.HashLine(line.TrimStart(), defines)) + + // Reset the state + { CurrentBuilder = StringBuilder() + LinesCollected = 0 + LastLineInfo = LastLineInfo.HashLine } + else + let nextBuilder = + if acc.LastLineInfo = LastLineInfo.Content then + acc.CurrentBuilder.Append(newline) + else + acc.CurrentBuilder + + let nextBuilder = nextBuilder.Append line + + { CurrentBuilder = nextBuilder + LinesCollected = acc.LinesCollected + 1 + LastLineInfo = LastLineInfo.Content }) + |> closeState + + fragmentsBuilder.Close() + +let mergeMultipleFormatResults config (results: (DefineCombination * FormatResult) list) : FormatResult = + let allInFragments: FormatResultForDefines list = + results + .AsParallel() + .Select(fun (dc, result) -> + let fragments = splitWhenHash dc config.EndOfLine.NewLineString result.Code + + { Result = result + Defines = dc + Fragments = fragments }) + |> Seq.toList + + let allHaveSameFragmentCount = + let allWithCount = List.map (fun { Fragments = f } -> f.Length) allInFragments + (Set allWithCount).Count = 1 + + if not allHaveSameFragmentCount then + let chunkReport = + allInFragments + |> List.map (fun result -> + sprintf "[%s] has %i fragments" (String.concat ", " result.Defines.Value) result.Fragments.Length) + |> String.concat config.EndOfLine.NewLineString + + raise ( + FormatException( + $"""Fantomas is trying to format the input multiple times due to the detection of multiple defines. +There is a problem with merging all the code back together. +{chunkReport} +Please raise an issue at https://fsprojects.github.io/fantomas-tools/#/fantomas/preview.""" + ) + ) + + // Go over each fragment of all results. + // Compare the fragments one by one and pick the one with most content. + // See custom comparison for CodeFragment. + let rec traverseFragments + (input: CodeFragment list list) + (continuation: CodeFragment list -> CodeFragment list) + : CodeFragment list = + let headItems = List.choose List.tryHead input + + if List.isEmpty headItems then + continuation [] + else + let max = List.max headItems + traverseFragments (List.map List.tail input) (fun xs -> max :: xs |> continuation) + + let selectedFragments: CodeFragment list = + traverseFragments (allInFragments |> List.map (fun r -> r.Fragments)) id + + let appendNewline (fragment: CodeFragment) (builder: StringBuilder) : StringBuilder = + match fragment with + | CodeFragment.NoContent _ -> builder + | CodeFragment.HashLine _ + | CodeFragment.Content _ -> builder.Append config.EndOfLine.NewLineString + + let appendContent (fragment: CodeFragment) (builder: StringBuilder) : StringBuilder = + match fragment with + | CodeFragment.NoContent _ -> builder + | CodeFragment.HashLine(line = content) + | CodeFragment.Content(code = content) -> builder.Append content + + let areThereNotCursors = + results |> List.forall (fun (_, result) -> Option.isNone result.Cursor) + + if areThereNotCursors then + let code = + (StringBuilder(), selectedFragments) + ||> List.foldWithLast + (fun acc fragment -> (appendContent fragment >> appendNewline fragment) acc) + (fun acc fragment -> appendContent fragment acc) + |> stringBuilderResult + + { Code = code; Cursor = None } + else + let weaver = + { LastLine = 1 + FoundCursor = None + ContentBuilder = StringBuilder() + Cursors = + results + |> List.choose (fun (dc, formatResult) -> formatResult.Cursor |> Option.map (fun cursor -> dc, cursor)) + |> Map.ofList } + + let finalResult = + let processFragment + (postContent: CodeFragment -> StringBuilder -> StringBuilder) + (acc: FragmentWeaverState) + (fragment: CodeFragment) + : FragmentWeaverState = + let nextLastLine = acc.LastLine + fragment.LineCount + + // Try and find a cursor for the current set of defines that falls within the range of the current block. + match Map.tryFind fragment.Defines acc.Cursors with + | Some cursor when (acc.LastLine <= cursor.Line && cursor.Line <= nextLastLine) -> + { acc with + LastLine = acc.LastLine + fragment.LineCount + ContentBuilder = (appendContent fragment >> postContent fragment) acc.ContentBuilder + FoundCursor = Some(fragment.Defines, cursor) } + | Some _ + | None -> + { acc with + LastLine = acc.LastLine + fragment.LineCount + ContentBuilder = (appendContent fragment >> postContent fragment) acc.ContentBuilder } + + (weaver, selectedFragments) + ||> List.foldWithLast (processFragment appendNewline) (processFragment (fun _ sb -> sb)) + + { Code = finalResult.ContentBuilder.ToString() + Cursor = Option.map snd finalResult.FoundCursor } diff --git a/src/Fantomas.Core/MultipleDefineCombinations.fsi b/src/Fantomas.Core/MultipleDefineCombinations.fsi new file mode 100644 index 0000000000..d787aa7126 --- /dev/null +++ b/src/Fantomas.Core/MultipleDefineCombinations.fsi @@ -0,0 +1,6 @@ +module internal Fantomas.Core.MultipleDefineCombinations + +/// When conditional defines were found in the source code, we format the code using all possible combinations. +/// Depending on the values of each combination, code will either be produced or not. +/// In this function, we try to piece back all the active code fragments. +val mergeMultipleFormatResults: config: FormatConfig -> results: (DefineCombination * FormatResult) list -> FormatResult diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index d6cf69ff8c..1faca42e7a 100644 --- a/src/Fantomas.Core/Selection.fs +++ b/src/Fantomas.Core/Selection.fs @@ -1,8 +1,6 @@ module internal Fantomas.Core.Selection open FSharp.Compiler.Text -open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas.Core.SyntaxOak open Fantomas.Core.ISourceTextExtensions @@ -165,12 +163,15 @@ let mkTreeWithSingleNode (node: Node) : TreeForSelection = | :? ExprArrayOrListNode as node -> let expr = Expr.ArrayOrList node mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) + | :? ExprInheritRecordNode as node -> + let expr = Expr.InheritRecord node + mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) + | :? ExprAnonStructRecordNode as node -> + let expr = Expr.AnonStructRecord node + mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) | :? ExprRecordNode as node -> let expr = Expr.Record node mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) - | :? ExprAnonRecordNode as node -> - let expr = Expr.AnonRecord node - mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) | :? ExprObjExprNode as node -> let expr = Expr.ObjExpr node mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) @@ -401,7 +402,7 @@ let formatSelection MaxLineLength = maxLineLength } let formattedSelection = - let context = Context.Context.Create selectionConfig + let context = Context.Context.Create true selectionConfig match tree with | TreeForSelection.Unsupported -> @@ -409,11 +410,15 @@ let formatSelection | TreeForSelection.Standalone tree -> let enrichedTree = Trivia.enrichTree selectionConfig sourceText baseUntypedTree tree - CodePrinter.genFile enrichedTree context |> Context.dump true + CodePrinter.genFile enrichedTree context + |> Context.dump true + |> fun result -> result.Code | TreeForSelection.RequiresExtraction(tree, t) -> let enrichedTree = Trivia.enrichTree selectionConfig sourceText baseUntypedTree tree - let formattedCode = CodePrinter.genFile enrichedTree context |> Context.dump true + let { Code = formattedCode } = + CodePrinter.genFile enrichedTree context |> Context.dump true + let source = SourceText.ofString formattedCode let formattedAST, _ = Fantomas.FCS.Parse.parseFile isSignature source [] let formattedTree = ASTTransformer.mkOak (Some source) formattedAST diff --git a/src/Fantomas.Core/Selection.fsi b/src/Fantomas.Core/Selection.fsi index 042e47619a..dde98cd52f 100644 --- a/src/Fantomas.Core/Selection.fsi +++ b/src/Fantomas.Core/Selection.fsi @@ -1,7 +1,6 @@ module internal Fantomas.Core.Selection open FSharp.Compiler.Text -open Fantomas.Core.FormatConfig val formatSelection: config: FormatConfig -> isSignature: bool -> selection: range -> sourceText: ISourceText -> Async diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index c9cbb770ae..4ff3c525b9 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -1,16 +1,15 @@ -module internal rec Fantomas.Core.SyntaxOak +module rec Fantomas.Core.SyntaxOak open System.Collections.Generic open FSharp.Compiler.Text -type DefineCombination = string list - type TriviaContent = | CommentOnSingleLine of string | LineCommentAfterSourceCode of comment: string | BlockComment of string * newlineBefore: bool * newlineAfter: bool | Newline | Directive of string + | Cursor type TriviaNode(content: TriviaContent, range: range) = member val Content = content @@ -26,9 +25,12 @@ type Node = abstract Children: Node array abstract AddBefore: triviaNode: TriviaNode -> unit abstract AddAfter: triviaNode: TriviaNode -> unit + abstract AddCursor: pos -> unit + abstract TryGetCursor: pos option [] type NodeBase(range: range) = + let mutable potentialCursor = None let nodesBefore = Queue(0) let nodesAfter = Queue(0) @@ -40,6 +42,8 @@ type NodeBase(range: range) = member _.AddBefore triviaNode = nodesBefore.Enqueue triviaNode member _.AddAfter triviaNode = nodesAfter.Enqueue triviaNode abstract member Children: Node array + member _.AddCursor cursor = potentialCursor <- Some cursor + member _.TryGetCursor = potentialCursor interface Node with member x.ContentBefore = x.ContentBefore @@ -50,6 +54,8 @@ type NodeBase(range: range) = member x.AddBefore triviaNode = x.AddBefore triviaNode member x.AddAfter triviaNode = x.AddAfter triviaNode member x.Children = x.Children + member x.AddCursor cursor = x.AddCursor cursor + member x.TryGetCursor = x.TryGetCursor type StringNode(content: string, range: range) = inherit NodeBase(range) @@ -374,11 +380,6 @@ type Type = | Or n -> n | LongIdentApp n -> n - member e.IsStroustrupStyleType: bool = - match e with - | AnonRecord _ -> true - | _ -> false - /// A pattern composed from a left hand-side pattern, a single text token/operator and a right hand-side pattern. type PatLeftMiddleRight(lhs: Pattern, middle: Choice, rhs: Pattern, range) = inherit NodeBase(range) @@ -738,18 +739,6 @@ type InheritConstructor = | Paren n -> n.InheritKeyword | Other n -> n.InheritKeyword -[] -type RecordNodeExtra = - | Inherit of inheritConstructor: InheritConstructor - | With of expr: Expr - | None - - static member Node(extra: RecordNodeExtra) : Node option = - match extra with - | Inherit n -> Some(InheritConstructor.Node n) - | With e -> Some(Expr.Node e) - | None -> Option.None - type RecordFieldNode(fieldName: IdentListNode, equals: SingleTextNode, expr: Expr, range) = inherit NodeBase(range) @@ -758,58 +747,76 @@ type RecordFieldNode(fieldName: IdentListNode, equals: SingleTextNode, expr: Exp member val Equals = equals member val Expr = expr +[] +type ExprRecordBaseNode(openingBrace: SingleTextNode, fields: RecordFieldNode list, closingBrace: SingleTextNode, range) + = + inherit NodeBase(range) + + member val OpeningBrace = openingBrace + member val Fields = fields + member val ClosingBrace = closingBrace + member x.HasFields = List.isNotEmpty x.Fields + +/// +/// Represents a record instance, parsed from both `SynExpr.Record` and `SynExpr.AnonRecd`. +/// type ExprRecordNode ( openingBrace: SingleTextNode, - extra: RecordNodeExtra, + copyInfo: Expr option, fields: RecordFieldNode list, closingBrace: SingleTextNode, range ) = - inherit NodeBase(range) + inherit ExprRecordBaseNode(openingBrace, fields, closingBrace, range) + + member val CopyInfo = copyInfo override val Children: Node array = [| yield openingBrace - yield! noa (RecordNodeExtra.Node extra) + yield! copyInfo |> Option.map Expr.Node |> noa yield! nodes fields yield closingBrace |] - member val OpeningBrace = openingBrace - member val Extra = extra - member val Fields = fields - member val ClosingBrace = closingBrace + member x.HasFields = List.isNotEmpty x.Fields -type AnonRecordFieldNode(ident: SingleTextNode, equals: SingleTextNode, rhs: Expr, range) = - inherit NodeBase(range) +type ExprAnonStructRecordNode + ( + structNode: SingleTextNode, + openingBrace: SingleTextNode, + copyInfo: Expr option, + fields: RecordFieldNode list, + closingBrace: SingleTextNode, + range + ) = + inherit ExprRecordNode(openingBrace, copyInfo, fields, closingBrace, range) + member val Struct = structNode - override val Children: Node array = [| yield ident; yield equals; yield Expr.Node rhs |] - member val Ident = ident - member val Equals = equals - member val Expr = rhs + override val Children: Node array = + [| yield structNode + yield openingBrace + yield! copyInfo |> Option.map Expr.Node |> noa + yield! nodes fields + yield closingBrace |] -type ExprAnonRecordNode +type ExprInheritRecordNode ( - isStruct: bool, openingBrace: SingleTextNode, - copyInfo: Expr option, - fields: AnonRecordFieldNode list, + inheritConstructor: InheritConstructor, + fields: RecordFieldNode list, closingBrace: SingleTextNode, range ) = - inherit NodeBase(range) + inherit ExprRecordBaseNode(openingBrace, fields, closingBrace, range) + + member val InheritConstructor = inheritConstructor override val Children: Node array = [| yield openingBrace - yield! noa (Option.map Expr.Node copyInfo) + yield InheritConstructor.Node inheritConstructor yield! nodes fields yield closingBrace |] - member val IsStruct = isStruct - member val OpeningBrace = openingBrace - member val CopyInfo = copyInfo - member val Fields = fields - member val ClosingBrace = closingBrace - type InterfaceImplNode ( interfaceNode: SingleTextNode, @@ -1300,6 +1307,8 @@ type ExprTryFinallyNode(tryNode: SingleTextNode, tryExpr: Expr, finallyNode: Sin member val FinallyExpr = finallyExpr type ElseIfNode(mElse: range, mIf: range, condition: Node, range) as elseIfNode = + let mutable elseCursor = None + let mutable ifCursor = None let nodesBefore = Queue(0) let nodesAfter = Queue(0) let mutable lastNodeAfterIsLineCommentAfterSource = false @@ -1318,7 +1327,9 @@ type ElseIfNode(mElse: range, mIf: range, condition: Node, range) as elseIfNode member _.AddAfter(triviaNode: TriviaNode) = (elseIfNode :> Node).AddAfter triviaNode - member _.Children = Array.empty } + member _.Children = Array.empty + member _.AddCursor cursor = elseCursor <- Some cursor + member _.TryGetCursor = elseCursor } let ifNode = { new Node with @@ -1337,7 +1348,9 @@ type ElseIfNode(mElse: range, mIf: range, condition: Node, range) as elseIfNode member _.AddAfter(triviaNode: TriviaNode) = (elseIfNode :> Node).AddAfter triviaNode - member _.Children = Array.empty } + member _.Children = Array.empty + member _.AddCursor cursor = ifCursor <- Some cursor + member _.TryGetCursor = ifCursor } interface Node with member _.ContentBefore: TriviaNode seq = nodesBefore @@ -1365,6 +1378,8 @@ type ElseIfNode(mElse: range, mIf: range, condition: Node, range) as elseIfNode nodesAfter.Enqueue triviaNode member val Children = [| elseNode; ifNode |] + member _.AddCursor _ = () + member _.TryGetCursor = None [] type IfKeywordNode = @@ -1597,7 +1612,8 @@ type Expr = | StructTuple of ExprStructTupleNode | ArrayOrList of ExprArrayOrListNode | Record of ExprRecordNode - | AnonRecord of ExprAnonRecordNode + | InheritRecord of ExprInheritRecordNode + | AnonStructRecord of ExprAnonStructRecordNode | ObjExpr of ExprObjExprNode | While of ExprWhileNode | For of ExprForNode @@ -1661,7 +1677,8 @@ type Expr = | StructTuple n -> n | ArrayOrList n -> n | Record n -> n - | AnonRecord n -> n + | InheritRecord n -> n + | AnonStructRecord n -> n | ObjExpr n -> n | While n -> n | For n -> n @@ -1712,24 +1729,6 @@ type Expr = | Typar n -> n | Chain n -> n - member e.IsStroustrupStyleExpr: bool = - match e with - | Expr.Record node -> - match node.Extra with - | RecordNodeExtra.Inherit _ - | RecordNodeExtra.With _ -> false - | RecordNodeExtra.None -> true - | Expr.AnonRecord node -> - match node.CopyInfo with - | Some _ -> false - | None -> true - | Expr.NamedComputation node -> - match node.Name with - | Expr.Ident _ -> true - | _ -> false - | Expr.ArrayOrList _ -> true - | _ -> false - member e.HasParentheses: bool = match e with | Expr.Paren _ -> true diff --git a/src/Fantomas.Core/Trivia.fs b/src/Fantomas.Core/Trivia.fs index 7306a413ac..da4678425c 100644 --- a/src/Fantomas.Core/Trivia.fs +++ b/src/Fantomas.Core/Trivia.fs @@ -3,7 +3,6 @@ open FSharp.Compiler.Syntax open FSharp.Compiler.SyntaxTrivia open FSharp.Compiler.Text -open Fantomas.Core.FormatConfig open Fantomas.Core.ISourceTextExtensions open Fantomas.Core.SyntaxOak @@ -188,7 +187,6 @@ let rec visitLastChildNode (node: Node) : Node = | :? ExprLetOrUseBangNode | :? ExprAndBang | :? BindingNode - | :? ModuleOrNamespaceNode | :? TypeDefnEnumNode | :? TypeDefnUnionNode | :? TypeDefnRecordNode @@ -228,8 +226,9 @@ let rec visitLastChildNode (node: Node) : Node = | :? BindingReturnInfoNode | :? PatLeftMiddleRight | :? MultipleAttributeListNode -> visitLastChildNode (Array.last node.Children) - | :? PatLongIdentNode as pat -> - if Seq.isEmpty pat.Children then + | :? PatLongIdentNode + | :? ModuleOrNamespaceNode -> + if Array.isEmpty node.Children then node else visitLastChildNode (Seq.last node.Children) @@ -310,7 +309,8 @@ let addToTree (tree: Oak) (trivia: TriviaNode seq) = | CommentOnSingleLine _ | Newline | Directive _ -> simpleTriviaToTriviaInstruction parentNode trivia - | BlockComment _ -> blockCommentToTriviaInstruction parentNode trivia + | BlockComment _ + | Cursor _ -> blockCommentToTriviaInstruction parentNode trivia let enrichTree (config: FormatConfig) (sourceText: ISourceText) (ast: ParsedInput) (tree: Oak) : Oak = let fullTreeRange = tree.Range @@ -342,3 +342,13 @@ let enrichTree (config: FormatConfig) (sourceText: ISourceText) (ast: ParsedInpu addToTree tree trivia tree + +let insertCursor (tree: Oak) (cursor: pos) = + let cursorRange = Range.mkRange (tree :> Node).Range.FileName cursor cursor + let nodeWithCursor = findNodeWhereRangeFitsIn tree cursorRange + + match nodeWithCursor with + | Some((:? SingleTextNode) as node) -> node.AddCursor cursor + | _ -> addToTree tree [| TriviaNode(TriviaContent.Cursor, cursorRange) |] + + tree diff --git a/src/Fantomas.Core/Trivia.fsi b/src/Fantomas.Core/Trivia.fsi index a362199b48..5ecb7272cb 100644 --- a/src/Fantomas.Core/Trivia.fsi +++ b/src/Fantomas.Core/Trivia.fsi @@ -2,8 +2,11 @@ module internal Fantomas.Core.Trivia open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open Fantomas.Core.FormatConfig open Fantomas.Core.SyntaxOak val findNodeWhereRangeFitsIn: root: Node -> range: range -> Node option val enrichTree: config: FormatConfig -> sourceText: ISourceText -> ast: ParsedInput -> tree: Oak -> Oak + +/// Try and insert a cursor position as Trivia inside the Oak +/// The cursor could either be inside a Node or floating around one. +val insertCursor: tree: Oak -> cursor: pos -> Oak diff --git a/src/Fantomas.Core/Utils.fs b/src/Fantomas.Core/Utils.fs index deec0b0ffa..810a8994d7 100644 --- a/src/Fantomas.Core/Utils.fs +++ b/src/Fantomas.Core/Utils.fs @@ -1,7 +1,6 @@ namespace Fantomas.Core open System -open System.Text.RegularExpressions open Microsoft.FSharp.Core.CompilerServices [] @@ -9,58 +8,6 @@ module String = let startsWithOrdinal (prefix: string) (str: string) = str.StartsWith(prefix, StringComparison.Ordinal) - let lengthWithoutSpaces (str: string) = str.Replace(" ", String.Empty).Length - - let hashRegex = @"^\s*#(if|elseif|else|endif).*" - - let private splitWhenHash (newline: string) (source: string) : string list = - let lines = source.Split([| newline |], options = StringSplitOptions.None) - - let hashLineIndexes = - lines - |> Array.mapi (fun idx line -> Regex.IsMatch(line, hashRegex), idx) - |> Array.choose (fun (isMatch, idx) -> if isMatch then Some idx else None) - |> Array.toList - - let hashLineIndexesWithStart = - match List.tryHead hashLineIndexes with - | Some 0 -> hashLineIndexes - | _ -> 0 :: hashLineIndexes - - let rec loop (indexes: int list) (finalContinuation: string[] list -> string[] list) = - match indexes with - | [] -> finalContinuation [] - | i1 :: i2 :: rest -> - let chunk = lines.[i1 .. (i2 - 1)] - chunk.[0] <- chunk.[0].TrimStart() - loop (i2 :: rest) (fun otherChunks -> chunk :: otherChunks |> finalContinuation) - | [ lastIndex ] -> - let chunk = lines.[lastIndex..] - chunk.[0] <- chunk.[0].TrimStart() - finalContinuation [ chunk ] - - loop hashLineIndexesWithStart id |> List.map (String.concat newline) - - let splitInFragments (newline: string) (items: (string list * string) list) : (string list * string list) list = - List.map - (fun (defines, code) -> - let fragments = splitWhenHash newline code - defines, fragments) - items - - let merge (aChunks: string list) (bChunks: string list) : string list = - List.zip aChunks bChunks - |> List.map (fun (a', b') -> - let la = lengthWithoutSpaces a' - let lb = lengthWithoutSpaces b' - - if la <> lb then - if la > lb then a' else b' - else if String.length a' < String.length b' then - a' - else - b') - let empty = String.Empty let isNotNullOrEmpty = String.IsNullOrEmpty >> not let isNotNullOrWhitespace = String.IsNullOrWhiteSpace >> not @@ -117,6 +64,20 @@ module List = visit list headList.Close() + let foldWithLast + (f: 'state -> 'item -> 'state) + (g: 'state -> 'item -> 'state) + (initialState: 'state) + (items: 'item list) + : 'state = + let rec visit acc xs = + match xs with + | [] -> acc + | [ last ] -> g acc last + | head :: tail -> visit (f acc head) tail + + visit initialState items + module Async = let map f computation = async.Bind(computation, f >> async.Return) diff --git a/src/Fantomas.Core/Utils.fsi b/src/Fantomas.Core/Utils.fsi index 16f65f0b52..74b2a2a86b 100644 --- a/src/Fantomas.Core/Utils.fsi +++ b/src/Fantomas.Core/Utils.fsi @@ -3,8 +3,6 @@ namespace Fantomas.Core [] module String = val startsWithOrdinal: prefix: string -> str: string -> bool - val splitInFragments: newline: string -> items: (string list * string) list -> (string list * string list) list - val merge: aChunks: string list -> bChunks: string list -> string list val empty: string val isNotNullOrEmpty: (string -> bool) val isNotNullOrWhitespace: (string -> bool) @@ -18,6 +16,14 @@ module List = /// Removes the last element of a list val cutOffLast: 'a list -> 'a list + /// Similar to a List.fold but pass in another fold function for when the last item is reached. + val foldWithLast: + f: ('state -> 'item -> 'state) -> + g: ('state -> 'item -> 'state) -> + initialState: 'state -> + items: 'item list -> + 'state + module Async = val map: f: ('a -> 'b) -> computation: Async<'a> -> Async<'b> diff --git a/src/Fantomas.Core/Validation.fs b/src/Fantomas.Core/Validation.fs index 4069de7821..41498503bb 100644 --- a/src/Fantomas.Core/Validation.fs +++ b/src/Fantomas.Core/Validation.fs @@ -46,7 +46,7 @@ let isValidFSharpCode (isSignature: bool) (source: string) : Async = let isValidForCombinations = defineCombinations |> List.map (fun defineCombination -> - let _, diagnostics = parseFile isSignature sourceText defineCombination + let _, diagnostics = parseFile isSignature sourceText defineCombination.Value noWarningOrErrorDiagnostics diagnostics) return Seq.forall id isValidForCombinations diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 962fd81924..cc60848d0d 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -2,7 +2,6 @@ module Fantomas.Tests.EditorConfigurationTests open System open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas open NUnit.Framework open System.IO @@ -14,7 +13,7 @@ let tempName () = Guid.NewGuid().ToString("N") type ConfigurationFile internal ( - config: FormatConfig.FormatConfig, + config: FormatConfig, rootFolderName: string, ?editorConfigHeader: string, ?subFolder: string, @@ -449,14 +448,13 @@ insert_final_newline = false Assert.IsFalse config.InsertFinalNewline [] -let ``fsharp_experimental_stroustrup_style = true`` () = +let ``fsharp_multiline_bracket_style = stroustrup`` () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_block_brackets_on_same_column = true -fsharp_experimental_stroustrup_style = true +fsharp_multiline_bracket_style = stroustrup """ use configFixture = @@ -466,16 +464,16 @@ fsharp_experimental_stroustrup_style = true let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(ExperimentalStroustrup, config.MultilineBracketStyle) + Assert.AreEqual(Stroustrup, config.MultilineBracketStyle) [] -let ``fsharp_multiline_bracket_style = experimental_stroustrup`` () = +let ``fsharp_multiline_bracket_style = aligned`` () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_bracket_style = experimental_stroustrup +fsharp_multiline_bracket_style = aligned """ use configFixture = @@ -485,16 +483,16 @@ fsharp_multiline_bracket_style = experimental_stroustrup let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(ExperimentalStroustrup, config.MultilineBracketStyle) + Assert.AreEqual(Aligned, config.MultilineBracketStyle) [] -let ``fsharp_multiline_bracket_style = aligned`` () = +let ``fsharp_multiline_bracket_style = cramped`` () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_bracket_style = aligned +fsharp_multiline_bracket_style = cramped """ use configFixture = @@ -504,16 +502,16 @@ fsharp_multiline_bracket_style = aligned let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(Aligned, config.MultilineBracketStyle) + Assert.AreEqual(Cramped, config.MultilineBracketStyle) [] -let ``fsharp_multiline_bracket_style = cramped`` () = +let fsharp_prefer_computation_expression_name_on_same_line () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_bracket_style = cramped +fsharp_newline_before_multiline_computation_expression = false """ use configFixture = @@ -523,16 +521,16 @@ fsharp_multiline_bracket_style = cramped let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(Cramped, config.MultilineBracketStyle) + Assert.IsFalse config.NewlineBeforeMultilineComputationExpression [] -let ``fsharp_multiline_block_brackets_on_same_column = true`` () = +let fsharp_stroustrup_final_list_arguments () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_block_brackets_on_same_column = true +fsharp_experimental_elmish = true """ use configFixture = @@ -542,4 +540,4 @@ fsharp_multiline_block_brackets_on_same_column = true let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(Aligned, config.MultilineBracketStyle) + Assert.IsTrue config.ExperimentalElmish diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index dbddd3d7c6..96ed543379 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -27,6 +27,7 @@ + diff --git a/src/Fantomas.Tests/FantomasServiceTests.fs b/src/Fantomas.Tests/FantomasServiceTests.fs new file mode 100644 index 0000000000..4b54ee54b6 --- /dev/null +++ b/src/Fantomas.Tests/FantomasServiceTests.fs @@ -0,0 +1,31 @@ +module Fantomas.CoreGlobalTool.Tests.FantomasServiceTests + +open System +open System.IO +open Fantomas.Client.Contracts +open Fantomas.Client.LSPFantomasServiceTypes +open Fantomas.Client.LSPFantomasService +open NUnit.Framework + +let toucheFileAndFormat (path: string) (service: FantomasService) : FantomasResponse = + let content = File.ReadAllText path + let dirtyContent = String.Concat(content, " ") + File.WriteAllText(path, dirtyContent) + + let request: FormatDocumentRequest = + { SourceCode = dirtyContent + FilePath = path + Config = None + Cursor = None } + + service.FormatDocumentAsync(request).Result + +[] +[] +let ``locate fantomas tool`` () = + let service: FantomasService = new LSPFantomasService() + + let response = + toucheFileAndFormat @"C:\Users\nojaf\Projects\fantomas\src\Fantomas.Core\FormatConfig.fs" service + + Assert.AreEqual(int FantomasResponseCode.Formatted, response.Code) diff --git a/src/Fantomas.Tests/Integration/ConfigTests.fs b/src/Fantomas.Tests/Integration/ConfigTests.fs index c931ec3463..ddaa753bc9 100644 --- a/src/Fantomas.Tests/Integration/ConfigTests.fs +++ b/src/Fantomas.Tests/Integration/ConfigTests.fs @@ -5,6 +5,12 @@ open NUnit.Framework open FsUnit open Fantomas.Tests.TestHelpers +[] +let DetailedVerbosity = "--verbosity d" + +[] +let NormalVerbosity = "--verbosity n" + [] let ``config file in working directory should not require relative prefix, 821`` () = use fileFixture = @@ -21,9 +27,10 @@ indent_size=2 """ ) - let { ExitCode = exitCode; Output = output } = runFantomasTool fileFixture.Filename + let args = sprintf "%s %s" DetailedVerbosity fileFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 0 - output |> should startWith (sprintf "Processing %s" fileFixture.Filename) + output |> should contain (sprintf "Processing %s" fileFixture.Filename) let result = System.IO.File.ReadAllText(fileFixture.Filename) result @@ -45,7 +52,8 @@ end_of_line=cr """ ) - let { ExitCode = exitCode; Output = output } = runFantomasTool fileFixture.Filename + let args = sprintf "%s %s" DetailedVerbosity fileFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 1 StringAssert.Contains("Carriage returns are not valid for F# code, please use one of 'lf' or 'crlf'", output) @@ -53,8 +61,7 @@ let valid_eol_settings = [ "lf"; "crlf" ] [] let ``uses end_of_line setting to write user newlines`` setting = - let newline = - (FormatConfig.EndOfLineStyle.OfConfigString setting).Value.NewLineString + let newline = (EndOfLineStyle.OfConfigString setting).Value.NewLineString let sampleCode nln = sprintf "let a = 9%s%slet b = 7%s" nln nln nln @@ -87,8 +94,7 @@ let ``end_of_line should be respected for ifdef`` () = use configFixture = new ConfigurationFile( - sprintf - """ + """ [*.fs] end_of_line = lf """ diff --git a/src/Fantomas.Tests/Integration/DaemonTests.fs b/src/Fantomas.Tests/Integration/DaemonTests.fs index 74730dd6ca..70c64383c5 100644 --- a/src/Fantomas.Tests/Integration/DaemonTests.fs +++ b/src/Fantomas.Tests/Integration/DaemonTests.fs @@ -42,7 +42,7 @@ let ``config request`` () = async { let! config = client.InvokeAsync(Methods.Configuration) |> Async.AwaitTask - FormatConfig.FormatConfig.Default + FormatConfig.Default |> Fantomas.EditorConfig.configToEditorConfig |> fun s -> s.Split('\n') |> Seq.map (fun line -> line.Split('=').[0]) @@ -59,14 +59,15 @@ let ``format implementation file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) |> Async.AwaitTask match response with - | FormatDocumentResponse.Formatted(_, formatted) -> + | FormatDocumentResponse.Formatted(formattedContent = formatted) -> assertFormatted formatted "module Foobar @@ -84,7 +85,8 @@ let ``format implementation file, unchanged`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = Some(readOnlyDict [ "end_of_line", "lf" ]) } + Config = Some(readOnlyDict [ "end_of_line", "lf" ]) + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) @@ -105,7 +107,8 @@ let ``format implementation file, error`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) @@ -127,7 +130,8 @@ let ``format implementation file, ignored file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) @@ -149,14 +153,15 @@ let ``format signature file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) |> Async.AwaitTask match response with - | FormatDocumentResponse.Formatted(_, formatted) -> + | FormatDocumentResponse.Formatted(formattedContent = formatted) -> assertFormatted formatted "module Foobar @@ -178,14 +183,15 @@ let ``format document respecting .editorconfig file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) |> Async.AwaitTask match response with - | FormatDocumentResponse.Formatted(_, formatted) -> + | FormatDocumentResponse.Formatted(formattedContent = formatted) -> assertFormatted formatted "module Foo @@ -208,14 +214,15 @@ let ``custom configuration has precedence over .editorconfig file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = Some(readOnlyDict [ "indent_size", "4" ]) } + Config = Some(readOnlyDict [ "indent_size", "4" ]) + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) |> Async.AwaitTask match response with - | FormatDocumentResponse.Formatted(_, formatted) -> + | FormatDocumentResponse.Formatted(formattedContent = formatted) -> assertFormatted formatted "module Foo @@ -303,14 +310,15 @@ let ``format document with both .editorconfig file and custom config`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = Some(readOnlyDict [ "fsharp_space_before_colon", "true" ]) } + Config = Some(readOnlyDict [ "fsharp_space_before_colon", "true" ]) + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) |> Async.AwaitTask match response with - | FormatDocumentResponse.Formatted(_, formatted) -> + | FormatDocumentResponse.Formatted(formattedContent = formatted) -> assertFormatted formatted "module Foo @@ -339,7 +347,8 @@ let ``format nested ignored file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename - Config = None } + Config = None + Cursor = None } let! response = client.InvokeAsync(Methods.FormatDocument, request) @@ -349,3 +358,32 @@ let ``format nested ignored file`` () = | FormatDocumentResponse.IgnoredFile _ -> Assert.Pass() | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" }) + +[] +let ``format cursor`` () = + runWithDaemon (fun client -> + async { + let sourceCode = + """ +let a = + "foobar" +""" + + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + Config = None + Cursor = Some(FormatCursorPosition(3, 8)) } + + let! response = + client.InvokeAsync(Methods.FormatDocument, request) + |> Async.AwaitTask + + match response with + | FormatDocumentResponse.Formatted(cursor = Some cursor) -> + Assert.AreEqual(1, cursor.Line) + Assert.AreEqual(12, cursor.Column) + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + }) diff --git a/src/Fantomas.Tests/Integration/ForceTests.fs b/src/Fantomas.Tests/Integration/ForceTests.fs index a49d5f0d5f..4a2ea76da1 100644 --- a/src/Fantomas.Tests/Integration/ForceTests.fs +++ b/src/Fantomas.Tests/Integration/ForceTests.fs @@ -5,6 +5,9 @@ open NUnit.Framework open FsUnit open Fantomas.Tests.TestHelpers +[] +let Verbosity = "--verbosity d" + // The day this test fails because Fantomas can format the file, is the day you can remove this file. [] @@ -17,7 +20,7 @@ let ``code that was invalid should be still be written`` () = use outputFixture = new OutputFile() let { ExitCode = exitCode; Output = output } = - runFantomasTool $"--force --out {outputFixture.Filename} {sourceFile}" + runFantomasTool $"{Verbosity} --force --out {outputFixture.Filename} {sourceFile}" exitCode |> should equal 0 output |> should contain "was not valid after formatting" diff --git a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs index 3f6043b76d..384c5cc096 100644 --- a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs +++ b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs @@ -8,6 +8,9 @@ open Fantomas.Tests.TestHelpers [] let Source = "let foo = 47" +[] +let Verbosity = "--verbosity d" + [] let ``ignore all fs files`` () = let fileName = "ToBeIgnored" @@ -31,8 +34,8 @@ let ``ignore specific file`` () = use inputFixture = new TemporaryFileCodeSample(Source, fileName = fileName) use ignoreFixture = new FantomasIgnoreFile("A.fs") - - let { ExitCode = exitCode; Output = output } = runFantomasTool inputFixture.Filename + let args = sprintf "%s %s" Verbosity inputFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 0 output |> should contain "was ignored" @@ -50,7 +53,7 @@ let ``ignore specific file in subfolder`` () = use ignoreFixture = new FantomasIgnoreFile(sprintf "%s/%s/A.fs" sub1 sub2) let { ExitCode = exitCode } = - runFantomasTool (sprintf "--recurse --check .%c%s" Path.DirectorySeparatorChar sub1) + runFantomasTool (sprintf "--check .%c%s" Path.DirectorySeparatorChar sub1) exitCode |> should equal 0 @@ -61,8 +64,8 @@ let ``don't ignore other files`` () = use inputFixture = new TemporaryFileCodeSample(Source, fileName = fileName) use ignoreFixture = new FantomasIgnoreFile("A.fs") - - let { ExitCode = exitCode; Output = output } = runFantomasTool inputFixture.Filename + let args = sprintf "%s %s" Verbosity inputFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 0 output |> should contain "Processing" @@ -79,12 +82,14 @@ let ``ignore file in folder`` () = use ignoreFixture = new FantomasIgnoreFile("A.fs") - let { ExitCode = exitCode } = - runFantomasTool (sprintf ".%c%s" Path.DirectorySeparatorChar subFolder) + let { ExitCode = exitCode; Output = output } = + runFantomasTool $"%s{Verbosity} .%c{Path.DirectorySeparatorChar}%s{subFolder}" exitCode |> should equal 0 File.ReadAllText inputFixture.Filename |> should equal Source + output |> should contain "A.fs was ignored" + [] let ``ignore file while checking`` () = let fileName = "A" @@ -94,7 +99,7 @@ let ``ignore file while checking`` () = use ignoreFixture = new FantomasIgnoreFile("A.fs") let { ExitCode = exitCode; Output = output } = - sprintf "%s --check" inputFixture.Filename |> runFantomasTool + sprintf "%s %s --check" Verbosity inputFixture.Filename |> runFantomasTool exitCode |> should equal 0 @@ -127,6 +132,7 @@ let ``honor ignore file when processing a folder`` () = use inputFixture = new FantomasIgnoreFile("*.fsx") let { Output = output } = - runFantomasTool (sprintf ".%c%s" Path.DirectorySeparatorChar subFolder) + runFantomasTool (sprintf "%s .%c%s" Verbosity Path.DirectorySeparatorChar subFolder) output |> should not' (contain "ignored") + output |> should contain "A.fs was formatted" diff --git a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs index 2b2cdec182..dbcf657fcc 100644 --- a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs +++ b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs @@ -11,6 +11,9 @@ let UserCode = "let a = 9" [] let FormattedCode = "let a = 9\n" +[] +let Verbosity = "--verbosity d" + let private fileContentMatches (expectedContent: string) (actualPath: string) : unit = if File.Exists(actualPath) then let actualContent = File.ReadAllText(actualPath) @@ -35,7 +38,7 @@ let ``format multiple paths`` () = fileContentMatches FormattedCode fileFixtureTwo.Filename [] -let ``format multiple paths with recursive flag`` () = +let ``format multiple paths recursively`` () = use config = new ConfigurationFile("[*]\nend_of_line = lf") use fileFixtureOne = new TemporaryFileCodeSample(UserCode) @@ -45,7 +48,7 @@ let ``format multiple paths with recursive flag`` () = use fileFixtureThree = new TemporaryFileCodeSample(UserCode, subFolder = "sub") let arguments = - sprintf "\"%s\" \"%s\" \"sub\" -r" fileFixtureOne.Filename fileFixtureTwo.Filename + sprintf "%s \"%s\" \"%s\" \"sub\"" Verbosity fileFixtureOne.Filename fileFixtureTwo.Filename let { ExitCode = exitCode; Output = output } = runFantomasTool arguments diff --git a/src/Fantomas.Tests/Integration/WriteTests.fs b/src/Fantomas.Tests/Integration/WriteTests.fs index 86721940db..d945ab4584 100644 --- a/src/Fantomas.Tests/Integration/WriteTests.fs +++ b/src/Fantomas.Tests/Integration/WriteTests.fs @@ -12,13 +12,16 @@ let FormattedCode = [] let UnformattedCode = "let a = 9" +[] +let Verbosity = "--verbosity d" + [] let ``correctly formatted file should not be written, 1984`` () = let fileName = "A" use inputFixture = new TemporaryFileCodeSample(FormattedCode, fileName = fileName) - - let { ExitCode = exitCode; Output = output } = runFantomasTool inputFixture.Filename + let args = sprintf "%s %s" Verbosity inputFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 0 output |> should contain "was unchanged" @@ -28,8 +31,8 @@ let ``incorrectly formatted file should be written`` () = let fileName = "A" use inputFixture = new TemporaryFileCodeSample(UnformattedCode, fileName = fileName) - - let { ExitCode = exitCode; Output = output } = runFantomasTool inputFixture.Filename + let args = sprintf "%s %s" Verbosity inputFixture.Filename + let { ExitCode = exitCode; Output = output } = runFantomasTool args exitCode |> should equal 0 output |> should contain "has been written" diff --git a/src/Fantomas.Tests/packages.lock.json b/src/Fantomas.Tests/packages.lock.json index a2b575530a..47f603d667 100644 --- a/src/Fantomas.Tests/packages.lock.json +++ b/src/Fantomas.Tests/packages.lock.json @@ -330,10 +330,15 @@ }, "Serilog": { "type": "Transitive", - "resolved": "2.8.0", - "contentHash": "zjuKXW5IQws43IHX7VY9nURsaCiBYh2kyJCWLJRSWrTsx/syBKHV8MibWe2A+QH3Er0AiwA+OJmO3DhFJDY1+A==", + "resolved": "2.12.0", + "contentHash": "xaiJLIdu6rYMKfQMYUZgTy8YK7SMZjB4Yk50C/u//Z4OsvxkUfSPJy4nknfvwAC34yr13q7kcyh4grbwhSxyZg==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "K6N5q+5fetjnJPvCmkWOpJ/V8IEIoMIB1s86OzBrbxwTyHxdx3pmz4H+8+O/Dc/ftUX12DM1aynx/dDowkwzqg==", "dependencies": { - "System.Collections.NonGeneric": "4.3.0" + "Serilog": "2.10.0" } }, "SerilogTraceListener": { @@ -344,6 +349,14 @@ "Serilog": "2.8.0" } }, + "Spectre.Console": { + "type": "Transitive", + "resolved": "0.46.1-preview.0.6", + "contentHash": "hJRBORvRHxxD3SjhnV7h0E6SY22iJVoP7oLtKz/YhVlNarMVOWe62qjQrk6+IF8M4D16Y+PC+D7C4W1rRLUCIg==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "StreamJsonRpc": { "type": "Transitive", "resolved": "2.8.28", @@ -397,19 +410,6 @@ "resolved": "5.0.0", "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==" }, - "System.Collections.NonGeneric": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "4.4.0", @@ -921,7 +921,10 @@ "Fantomas.Client": "[1.0.0, )", "Fantomas.Core": "[1.0.0, )", "Ignore": "[0.1.46, )", + "Serilog": "[2.12.0, )", + "Serilog.Sinks.Console": "[4.1.0, )", "SerilogTraceListener": "[3.2.1-dev-00011, )", + "Spectre.Console": "[0.46.1-preview.0.6, )", "StreamJsonRpc": "[2.8.28, )", "System.IO.Abstractions": "[17.2.3, )", "Thoth.Json.Net": "[8.0.0, )", diff --git a/src/Fantomas/Daemon.fs b/src/Fantomas/Daemon.fs index 3e0c43635c..f8169b28aa 100644 --- a/src/Fantomas/Daemon.fs +++ b/src/Fantomas/Daemon.fs @@ -12,18 +12,16 @@ open FSharp.Compiler.Text open Fantomas.Client.Contracts open Fantomas.Client.LSPFantomasServiceTypes open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas.EditorConfig type FantomasDaemon(sender: Stream, reader: Stream) as this = let rpc: JsonRpc = JsonRpc.Attach(sender, reader, this) + let traceListener = new DefaultTraceListener() do // hook up request/response logging for debugging rpc.TraceSource <- TraceSource(typeof.Name, SourceLevels.Verbose) - - rpc.TraceSource.Listeners.Add(new SerilogTraceListener.SerilogTraceListener(typeof.Name)) - |> ignore + rpc.TraceSource.Listeners.Add traceListener |> ignore let disconnectEvent = new ManualResetEvent(false) @@ -34,7 +32,9 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = do rpc.Disconnected.Add(fun _ -> exit ()) interface IDisposable with - member this.Dispose() = disconnectEvent.Dispose() + member this.Dispose() = + traceListener.Dispose() + disconnectEvent.Dispose() /// returns a hot task that resolves when the stream has terminated member this.WaitForClose = rpc.Completion @@ -59,14 +59,30 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = parseOptionsFromEditorConfig config configProperties | None -> readConfiguration request.FilePath - try - let! formatted = - CodeFormatter.FormatDocumentAsync(request.IsSignatureFile, request.SourceCode, config) + let cursor = + request.Cursor + |> Option.map (fun cursor -> CodeFormatter.MakePosition(cursor.Line, cursor.Column)) - if formatted = request.SourceCode then + try + let! formatResponse = + match cursor with + | None -> CodeFormatter.FormatDocumentAsync(request.IsSignatureFile, request.SourceCode, config) + | Some cursor -> + CodeFormatter.FormatDocumentAsync( + request.IsSignatureFile, + request.SourceCode, + config, + cursor + ) + + if formatResponse.Code = request.SourceCode then return FormatDocumentResponse.Unchanged request.FilePath else - return FormatDocumentResponse.Formatted(request.FilePath, formatted) + let cursor = + formatResponse.Cursor + |> Option.map (fun cursorPos -> FormatCursorPosition(cursorPos.Line, cursorPos.Column)) + + return FormatDocumentResponse.Formatted(request.FilePath, formatResponse.Code, cursor) with ex -> return FormatDocumentResponse.Error(request.FilePath, ex.Message) } @@ -109,7 +125,7 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = [] member _.Configuration() : string = let settings = - Reflection.getRecordFields FormatConfig.FormatConfig.Default + Reflection.getRecordFields FormatConfig.Default |> Array.toList |> List.choose (fun (recordField, defaultValue) -> let optionalField key value = diff --git a/src/Fantomas/EditorConfig.fs b/src/Fantomas/EditorConfig.fs index 41870ece44..8f5e4d6908 100644 --- a/src/Fantomas/EditorConfig.fs +++ b/src/Fantomas/EditorConfig.fs @@ -2,7 +2,7 @@ module Fantomas.EditorConfig open System.Collections.Generic open System.ComponentModel -open Fantomas.Core.FormatConfig +open Fantomas.Core module Reflection = open System @@ -75,24 +75,6 @@ let private (|Boolean|_|) b = elif b = "false" then Some(box false) else None -let private (|OldStroustrup|OldAligned|Unspecified|) (input: IReadOnlyDictionary) = - let toOption = - function - | true, "true" -> Some true - | true, "false" -> Some false - | _ -> None - - let hasStroustrup = - input.TryGetValue("fsharp_experimental_stroustrup_style") |> toOption - - let hasAligned = - input.TryGetValue("fsharp_multiline_block_brackets_on_same_column") |> toOption - - match hasAligned, hasStroustrup with - | Some true, Some true -> OldStroustrup - | Some true, _ -> OldAligned - | _ -> Unspecified - let parseOptionsFromEditorConfig (fallbackConfig: FormatConfig) (editorConfigProperties: IReadOnlyDictionary) @@ -109,18 +91,7 @@ let parseOptionsFromEditorConfig |> fun newValues -> let formatConfigType = FormatConfig.Default.GetType() - - let config = - Microsoft.FSharp.Reflection.FSharpValue.MakeRecord(formatConfigType, newValues) :?> FormatConfig - - match editorConfigProperties with - | Unspecified -> config - | OldStroustrup -> - { config with - MultilineBracketStyle = ExperimentalStroustrup } - | OldAligned -> - { config with - MultilineBracketStyle = Aligned } + Microsoft.FSharp.Reflection.FSharpValue.MakeRecord(formatConfigType, newValues) :?> FormatConfig let configToEditorConfig (config: FormatConfig) : string = Reflection.getRecordFields config diff --git a/src/Fantomas/EditorConfig.fsi b/src/Fantomas/EditorConfig.fsi index 849030ad42..814eaa0597 100644 --- a/src/Fantomas/EditorConfig.fsi +++ b/src/Fantomas/EditorConfig.fsi @@ -1,5 +1,7 @@ module Fantomas.EditorConfig +open Fantomas.Core + module Reflection = type FSharpRecordField = @@ -15,12 +17,12 @@ val supportedProperties: string list val toEditorConfigName: value: seq -> string val parseOptionsFromEditorConfig: - fallbackConfig: Core.FormatConfig.FormatConfig -> + fallbackConfig: FormatConfig -> editorConfigProperties: System.Collections.Generic.IReadOnlyDictionary -> - Core.FormatConfig.FormatConfig + FormatConfig -val configToEditorConfig: config: Core.FormatConfig.FormatConfig -> string +val configToEditorConfig: config: FormatConfig -> string -val tryReadConfiguration: fsharpFile: string -> Core.FormatConfig.FormatConfig option +val tryReadConfiguration: fsharpFile: string -> FormatConfig option -val readConfiguration: fsharpFile: string -> Core.FormatConfig.FormatConfig +val readConfiguration: fsharpFile: string -> FormatConfig diff --git a/src/Fantomas/Fantomas.fsproj b/src/Fantomas/Fantomas.fsproj index 9b5c233e04..6ad1b03514 100644 --- a/src/Fantomas/Fantomas.fsproj +++ b/src/Fantomas/Fantomas.fsproj @@ -12,6 +12,7 @@ true Fantomas false + --test:ParallelCheckingWithSignatureFilesOn @@ -20,6 +21,8 @@ + + @@ -33,6 +36,8 @@ + + @@ -40,5 +45,6 @@ + \ No newline at end of file diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index ee7e276eed..8eae2e903d 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -1,138 +1,168 @@ -module Fantomas.Format +namespace Fantomas open System open System.IO open Fantomas.Core -open Fantomas.Core.FormatConfig - -exception CodeFormatException of (string * Option) array with - override x.ToString() = - let errors = - x.Data0 - |> Array.choose (fun z -> - match z with - | file, Some ex -> Some(file, ex) - | _ -> None) - |> Array.map (fun z -> - let file, ex = z - file + ":\r\n" + ex.Message + "\r\n\r\n") - - let files = - x.Data0 - |> Array.map (fun z -> - match z with - | file, Some _ -> file + " !" - | file, None -> file) - - String.Join(String.Empty, errors) - + "The following files aren't formatted properly:" - + "\r\n- " - + String.Join("\r\n- ", files) + +type ProfileInfo = { LineCount: int; TimeTaken: TimeSpan } type FormatResult = - | Formatted of filename: string * formattedContent: string - | Unchanged of filename: string + | Formatted of filename: string * formattedContent: string * profileInfo: ProfileInfo option + | Unchanged of filename: string * profileInfo: ProfileInfo option | InvalidCode of filename: string * formattedContent: string | Error of filename: string * formattingError: Exception | IgnoredFile of filename: string -let private formatContentInternalAsync - (compareWithoutLineEndings: bool) - (config: FormatConfig) - (file: string) - (originalContent: string) - : Async = - if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then - async { return IgnoredFile file } - else - async { - try - let isSignatureFile = Path.GetExtension(file) = ".fsi" +type FormatParams = + { Config: FormatConfig + CompareWithoutLineEndings: bool + Profile: bool + File: string } - let! formattedContent = CodeFormatter.FormatDocumentAsync(isSignatureFile, originalContent, config) + static member Create(config: FormatConfig, compareWithoutLineEndings: bool, profile: bool, file: string) = + { Config = config + CompareWithoutLineEndings = compareWithoutLineEndings + Profile = profile + File = file } - let contentChanged = - if compareWithoutLineEndings then - let stripNewlines (s: string) = - System.Text.RegularExpressions.Regex.Replace(s, @"\r", String.Empty) + static member Create(compareWithoutLineEndings: bool, profile: bool, file: string) = + { Config = EditorConfig.readConfiguration file + CompareWithoutLineEndings = compareWithoutLineEndings + Profile = profile + File = file } - (stripNewlines originalContent) <> (stripNewlines formattedContent) - else - originalContent <> formattedContent +type CheckResult = + { Errors: (string * exn) list + Formatted: string list } - if contentChanged then - let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isSignatureFile, formattedContent) + member this.HasErrors = List.isNotEmpty this.Errors + member this.NeedsFormatting = List.isNotEmpty this.Formatted + member this.IsValid = List.isEmpty this.Errors && List.isEmpty this.Formatted - if not isValid then - return InvalidCode(filename = file, formattedContent = formattedContent) +module Format = + + let private formatContentInternalAsync + (formatParams: FormatParams) + (originalContent: string) + : Async = + if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) formatParams.File then + async { return IgnoredFile formatParams.File } + else + async { + try + let isSignatureFile = Path.GetExtension(formatParams.File) = ".fsi" + + let! { Code = formattedContent }, profileInfo = + if formatParams.Profile then + async { + let sw = Diagnostics.Stopwatch.StartNew() + + let! res = + CodeFormatter.FormatDocumentAsync( + isSignatureFile, + originalContent, + formatParams.Config + ) + + sw.Stop() + + let count = + originalContent.Length - originalContent.Replace(Environment.NewLine, "").Length + + let profileInfo = + { LineCount = count + TimeTaken = sw.Elapsed } + + return res, Some profileInfo + } + else + async { + let! res = + CodeFormatter.FormatDocumentAsync( + isSignatureFile, + originalContent, + formatParams.Config + ) + + return res, None + } + + let contentChanged = + if formatParams.CompareWithoutLineEndings then + let stripNewlines (s: string) = + System.Text.RegularExpressions.Regex.Replace(s, @"\r", String.Empty) + + (stripNewlines originalContent) <> (stripNewlines formattedContent) + else + originalContent <> formattedContent + + if contentChanged then + let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isSignatureFile, formattedContent) + + if not isValid then + return InvalidCode(filename = formatParams.File, formattedContent = formattedContent) + else + return + Formatted( + filename = formatParams.File, + formattedContent = formattedContent, + profileInfo = profileInfo + ) else - return Formatted(filename = file, formattedContent = formattedContent) - else - return Unchanged(filename = file) - with ex -> - return Error(file, ex) - } + return Unchanged(filename = formatParams.File, profileInfo = profileInfo) + with ex -> + return Error(formatParams.File, ex) + } + + let formatContentAsync = formatContentInternalAsync -let formatContentAsync = formatContentInternalAsync false + let private formatFileInternalAsync (parms: FormatParams) = + if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) parms.File then + async { return IgnoredFile parms.File } + else -let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: string) = - let config = EditorConfig.readConfiguration file + async { + let! originalContent = File.ReadAllTextAsync parms.File |> Async.AwaitTask - if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then - async { return IgnoredFile file } - else - let originalContent = File.ReadAllText file + let! formatted = originalContent |> formatContentInternalAsync parms + return formatted + } + + let formatFileAsync = formatFileInternalAsync + + /// Runs a check on the given files and reports the result to the given output: + /// + /// * It shows the paths of the files that need formatting + /// * It shows the path and the error message of files that failed the format check + /// + /// Returns: + /// + /// A record with the file names that were formatted and the files that encounter problems while formatting. + let checkCode (filenames: seq) = async { let! formatted = - originalContent - |> formatContentInternalAsync compareWithoutLineEndings config file + filenames + |> Seq.filter (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) >> not) + |> Seq.map (fun f -> formatFileInternalAsync (FormatParams.Create(true, false, f))) + |> Async.Parallel - return formatted - } + let getChangedFile = + function + | FormatResult.Unchanged _ + | FormatResult.IgnoredFile _ -> None + | FormatResult.Formatted(f, _, _) + | FormatResult.Error(f, _) + | FormatResult.InvalidCode(f, _) -> Some f -let formatFileAsync = formatFileInternalAsync false + let changes = formatted |> Seq.choose getChangedFile |> Seq.toList -type CheckResult = - { Errors: (string * exn) list - Formatted: string list } + let getErrors = + function + | FormatResult.Error(f, e) -> Some(f, e) + | _ -> None - member this.HasErrors = List.isNotEmpty this.Errors - member this.NeedsFormatting = List.isNotEmpty this.Formatted - member this.IsValid = List.isEmpty this.Errors && List.isEmpty this.Formatted + let errors = formatted |> Seq.choose getErrors |> Seq.toList -/// Runs a check on the given files and reports the result to the given output: -/// -/// * It shows the paths of the files that need formatting -/// * It shows the path and the error message of files that failed the format check -/// -/// Returns: -/// -/// A record with the file names that were formatted and the files that encounter problems while formatting. -let checkCode (filenames: seq) = - async { - let! formatted = - filenames - |> Seq.filter (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) >> not) - |> Seq.map (formatFileInternalAsync true) - |> Async.Parallel - - let getChangedFile = - function - | FormatResult.Unchanged _ - | FormatResult.IgnoredFile _ -> None - | FormatResult.Formatted(f, _) - | FormatResult.Error(f, _) - | FormatResult.InvalidCode(f, _) -> Some f - - let changes = formatted |> Seq.choose getChangedFile |> Seq.toList - - let getErrors = - function - | FormatResult.Error(f, e) -> Some(f, e) - | _ -> None - - let errors = formatted |> Seq.choose getErrors |> Seq.toList - - return { Errors = errors; Formatted = changes } - } + return { Errors = errors; Formatted = changes } + } diff --git a/src/Fantomas/Format.fsi b/src/Fantomas/Format.fsi index 9d5aa64ca0..0e338f89b0 100644 --- a/src/Fantomas/Format.fsi +++ b/src/Fantomas/Format.fsi @@ -1,19 +1,25 @@ -module Fantomas.Format +namespace Fantomas open System +open Fantomas.Core -exception CodeFormatException of (string * Option) array +type ProfileInfo = { LineCount: int; TimeTaken: TimeSpan } type FormatResult = - | Formatted of filename: string * formattedContent: string - | Unchanged of filename: string + | Formatted of filename: string * formattedContent: string * profileInfo: ProfileInfo option + | Unchanged of filename: string * profileInfo: ProfileInfo option | InvalidCode of filename: string * formattedContent: string | Error of filename: string * formattingError: Exception | IgnoredFile of filename: string -val formatContentAsync: (Core.FormatConfig.FormatConfig -> string -> string -> Async) +type FormatParams = + { Config: FormatConfig + CompareWithoutLineEndings: bool + Profile: bool + File: string } -val formatFileAsync: (string -> Async) + static member Create: bool * bool * string -> FormatParams + static member Create: FormatConfig * bool * bool * string -> FormatParams type CheckResult = { Errors: (string * exn) list @@ -25,12 +31,17 @@ type CheckResult = member NeedsFormatting: bool -/// Runs a check on the given files and reports the result to the given output: -/// -/// * It shows the paths of the files that need formatting -/// * It shows the path and the error message of files that failed the format check -/// -/// Returns: -/// -/// A record with the file names that were formatted and the files that encounter problems while formatting. -val checkCode: filenames: seq -> Async +module Format = + val formatContentAsync: (FormatParams -> string -> Async) + + val formatFileAsync: (FormatParams -> Async) + + /// Runs a check on the given files and reports the result to the given output: + /// + /// * It shows the paths of the files that need formatting + /// * It shows the path and the error message of files that failed the format check + /// + /// Returns: + /// + /// A record with the file names that were formatted and the files that encounter problems while formatting. + val checkCode: filenames: seq -> Async diff --git a/src/Fantomas/IgnoreFile.fs b/src/Fantomas/IgnoreFile.fs index f3fc544879..e7897d445e 100644 --- a/src/Fantomas/IgnoreFile.fs +++ b/src/Fantomas/IgnoreFile.fs @@ -2,6 +2,7 @@ namespace Fantomas open System.IO.Abstractions open Ignore +open Fantomas.Logging type AbsoluteFilePath = private @@ -89,5 +90,5 @@ module IgnoreFile = try ignoreFile.IsIgnored fullPath with ex -> - printfn "%A" ex + elog $"%A{ex}" false diff --git a/src/Fantomas/Logging.fs b/src/Fantomas/Logging.fs new file mode 100644 index 0000000000..c6ebda69a0 --- /dev/null +++ b/src/Fantomas/Logging.fs @@ -0,0 +1,34 @@ +module Fantomas.Logging + +open Serilog + +[] +type VerbosityLevel = + | Normal + | Detailed + +let initLogger (level: VerbosityLevel) : VerbosityLevel = + let logger = + match level with + | VerbosityLevel.Normal -> + LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console(outputTemplate = "{Message:lj}{NewLine}{Exception}") + .CreateLogger() + | VerbosityLevel.Detailed -> LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger() + + Log.Logger <- logger + level + +let logger = Log.Logger + +/// log a message +let stdlog (s: string) = logger.Information(s) + +/// log an error +let elog (s: string) = logger.Error(s) + +/// log a message if the verbosity level is >= Detailed +let logGrEqDetailed s = logger.Debug(s) + +let closeAndFlushLog () = Log.CloseAndFlush() diff --git a/src/Fantomas/Logging.fsi b/src/Fantomas/Logging.fsi new file mode 100644 index 0000000000..3eb3e57997 --- /dev/null +++ b/src/Fantomas/Logging.fsi @@ -0,0 +1,19 @@ +module Fantomas.Logging + +[] +type VerbosityLevel = + | Normal + | Detailed + +val initLogger: level: VerbosityLevel -> VerbosityLevel + +/// log a message +val stdlog: s: string -> unit + +/// log an error +val elog: s: string -> unit + +/// log a message if the verbosity level is >= Detailed +val logGrEqDetailed: s: string -> unit + +val closeAndFlushLog: unit -> unit diff --git a/src/Fantomas/Program.fs b/src/Fantomas/Program.fs index f45a8de250..7852593b30 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -3,26 +3,26 @@ open System.IO open Fantomas.Core open Fantomas open Fantomas.Daemon +open Fantomas.Logging open Argu open System.Text -open Fantomas.Format +open Spectre.Console let extensions = set [| ".fs"; ".fsx"; ".fsi"; ".ml"; ".mli" |] type Arguments = - | [] Recurse | [] Force | [] Profile | [] Out of string | [] Check | [] Daemon - | [] Version + | [] Version + | [] Verbosity of string | [] Input of string list interface IArgParserTemplate with member s.Usage = match s with - | Recurse -> "Process the input folder recursively." | Force -> "Print the output even if it is not valid F# code. For debugging purposes only." | Out _ -> "Give a valid path for files/folders. Files should have .fs, .fsx, .fsi, .ml or .mli extension only. Multiple files/folders are not supported." @@ -35,13 +35,7 @@ type Arguments = sprintf "Input paths: can be multiple folders or files with %s extension." (Seq.map (fun s -> "*" + s) extensions |> String.concat ",") - -let time f = - let sw = Diagnostics.Stopwatch.StartNew() - let res = f () - sw.Stop() - printfn "Time taken: %O s" sw.Elapsed - res + | Verbosity _ -> "Set the verbosity level. Allowed values are n[ormal] and d[etailed]." [] type InputPath = @@ -57,6 +51,12 @@ type OutputPath = | IO of string | NotKnown +type Table with + + member x.SetBorder(border: TableBorder) = + x.Border <- border + x + let isInExcludedDir (fullPath: string) = set [| "obj"; ".fable"; "fable_modules"; "node_modules" |] |> Set.map (fun dir -> sprintf "%c%s%c" Path.DirectorySeparatorChar dir Path.DirectorySeparatorChar) @@ -65,117 +65,137 @@ let isInExcludedDir (fullPath: string) = let isFSharpFile (s: string) = Set.contains (Path.GetExtension s) extensions -/// Get all appropriate files, either recursively or non-recursively -let rec allFiles isRec path = - let searchOption = - (if isRec then - SearchOption.AllDirectories - else - SearchOption.TopDirectoryOnly) +/// Get all appropriate files, recursively. +let findAllFilesRecursively path = + let searchOption = SearchOption.AllDirectories Directory.GetFiles(path, "*.*", searchOption) - |> Seq.filter (fun f -> - isFSharpFile f - && not (isInExcludedDir f) - && not (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f)) + |> Seq.filter (fun f -> isFSharpFile f && not (isInExcludedDir f)) /// Fantomas assumes the input files are UTF-8 /// As is stated in F# language spec: https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf#page=25 let private hasByteOrderMark file = - if File.Exists(file) then - let preamble = Encoding.UTF8.GetPreamble() + async { + if File.Exists(file) then + let preamble = Encoding.UTF8.GetPreamble() - use file = new FileStream(file, FileMode.Open, FileAccess.Read) + use file = new FileStream(file, FileMode.Open, FileAccess.Read) - let mutable bom = Array.zeroCreate 3 - file.Read(bom, 0, 3) |> ignore - bom = preamble - else - false + let mutable bom = Array.zeroCreate 3 + do! file.ReadAsync(bom, 0, 3) |> Async.AwaitTask |> Async.Ignore + return bom = preamble + else + return false + } + +let private invalidResultException file = + FormatException($"Formatting {file} leads to invalid F# code") /// Format a source string using given config and write to a text writer -let processSourceString (force: bool) s (fileName: string) config = +let processSourceString (force: bool) (profile: bool) s (fileName: string) config = let writeResult (formatted: string) = - if hasByteOrderMark fileName then - File.WriteAllText(fileName, formatted, Encoding.UTF8) - else - File.WriteAllText(fileName, formatted) + async { + let! hasBom = hasByteOrderMark fileName - printfn $"%s{fileName} has been written." + if hasBom then + do! File.WriteAllTextAsync(fileName, formatted, Encoding.UTF8) |> Async.AwaitTask + else + do! File.WriteAllTextAsync(fileName, formatted) |> Async.AwaitTask + + logGrEqDetailed $"%s{fileName} has been written." + } async { - let! formatted = s |> Format.formatContentAsync config fileName + let formatParams = FormatParams.Create(config, false, profile, fileName) + let! formatted = s |> Format.formatContentAsync formatParams match formatted with - | Format.FormatResult.Formatted(_, formattedContent) -> formattedContent |> writeResult - | Format.InvalidCode(file, formattedContent) when force -> - printfn $"%s{file} was not valid after formatting." - formattedContent |> writeResult - | Format.FormatResult.Unchanged file -> printfn $"'%s{file}' was unchanged" - | Format.IgnoredFile file -> printfn $"'%s{file}' was ignored" - | Format.FormatResult.Error(_, ex) -> raise ex - | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") + | FormatResult.Formatted(_, formattedContent, _) as r -> + do! formattedContent |> writeResult + return r + | FormatResult.InvalidCode(file, formattedContent) when force -> + stdlog $"%s{file} was not valid after formatting." + do! formattedContent |> writeResult + return FormatResult.Formatted(fileName, formattedContent, None) + | FormatResult.Unchanged(file, _) as r -> + logGrEqDetailed $"'%s{file}' was unchanged" + return r + | FormatResult.IgnoredFile file as r -> + logGrEqDetailed $"'%s{file}' was ignored" + return r + | FormatResult.Error _ as r -> return r + | FormatResult.InvalidCode(file, _) -> + let ex = invalidResultException file + return FormatResult.Error(file, ex) } - |> Async.RunSynchronously /// Format inFile and write to text writer -let processSourceFile (force: bool) inFile (tw: TextWriter) = +let processSourceFile (force: bool) (profile: bool) inFile (tw: TextWriter) = async { - let! formatted = Format.formatFileAsync inFile + let! formatted = FormatParams.Create(false, profile, inFile) |> Format.formatFileAsync match formatted with - | Format.FormatResult.Formatted(_, formattedContent) -> tw.Write(formattedContent) - | Format.InvalidCode(file, formattedContent) when force -> - printfn $"%s{file} was not valid after formatting." - tw.Write(formattedContent) - | Format.FormatResult.Unchanged _ -> inFile |> File.ReadAllText |> tw.Write - | Format.IgnoredFile file -> printfn $"'%s{file}' was ignored" - | Format.FormatResult.Error(_, ex) -> raise ex - | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") + | FormatResult.Formatted(_, formattedContent, _) as r -> + do! tw.WriteAsync(formattedContent) |> Async.AwaitTask + return r + | FormatResult.InvalidCode(file, formattedContent) when force -> + stdlog $"%s{file} was not valid after formatting." + do! tw.WriteAsync(formattedContent) |> Async.AwaitTask + return FormatResult.Formatted(inFile, formattedContent, None) + | FormatResult.Unchanged _ as r -> + let! input = inFile |> File.ReadAllTextAsync |> Async.AwaitTask + do! input |> tw.WriteAsync |> Async.AwaitTask + return r + | FormatResult.IgnoredFile file as r -> + logGrEqDetailed $"'%s{file}' was ignored" + return r + | FormatResult.Error _ as r -> return r + | FormatResult.InvalidCode(file, _) -> + let ex = invalidResultException file + return FormatResult.Error(file, ex) } - |> Async.RunSynchronously -let private reportCheckResults (output: TextWriter) (checkResult: Format.CheckResult) = +let private reportCheckResults (checkResult: CheckResult) = checkResult.Errors - |> List.map (fun (filename, exn) -> sprintf "error: Failed to format %s: %s" filename (exn.ToString())) - |> Seq.iter output.WriteLine + |> List.map (fun (filename, exn) -> $"error: Failed to format %s{filename}: %s{exn.ToString()}") + |> Seq.iter elog checkResult.Formatted - |> List.map (sprintf "%s needs formatting") - |> Seq.iter output.WriteLine + |> List.map (fun filename -> $"%s{filename} needs formatting") + |> Seq.iter stdlog -let runCheckCommand (recurse: bool) (inputPath: InputPath) : int = +let runCheckCommand (inputPath: InputPath) : int = let check files = Async.RunSynchronously(Format.checkCode files) - let processCheckResult (checkResult: Format.CheckResult) = + let processCheckResult (checkResult: CheckResult) = if checkResult.IsValid then - stdout.WriteLine "No changes required." + logGrEqDetailed "No changes required." 0 else - reportCheckResults stdout checkResult + reportCheckResults checkResult if checkResult.HasErrors then 1 else 99 match inputPath with | InputPath.NoFSharpFile s -> - eprintfn "Input path '%s' is unsupported file type" s + elog $"Input path '%s{s}' is unsupported file type" 1 | InputPath.NotFound s -> - eprintfn "Input path '%s' not found" s + elog $"Input path '%s{s}' not found" 1 | InputPath.Unspecified _ -> - eprintfn "No input path provided. Call with --help for usage information." + elog "No input path provided. Call with --help for usage information." 1 | InputPath.File f when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> - printfn "'%s' was ignored" f + logGrEqDetailed $"'%s{f}' was ignored" 0 | InputPath.File path -> path |> Seq.singleton |> check |> processCheckResult - | InputPath.Folder path -> path |> allFiles recurse |> check |> processCheckResult + | InputPath.Folder path -> path |> findAllFilesRecursively |> check |> processCheckResult | InputPath.Multiple(files, folders) -> let allFilesToCheck = seq { yield! files - yield! (Seq.collect (allFiles recurse) folders) + yield! (Seq.collect findAllFilesRecursively folders) } allFilesToCheck |> check |> processCheckResult @@ -236,14 +256,29 @@ let main argv = let force = results.Contains <@ Arguments.Force @> let profile = results.Contains <@ Arguments.Profile @> - let recurse = results.Contains <@ Arguments.Recurse @> - let version = results.TryGetResult <@ Arguments.Version @> + let maybeVerbosity = + results.TryGetResult <@ Arguments.Verbosity @> + |> Option.map (fun v -> v.ToLowerInvariant()) + + let verbosity = + match maybeVerbosity with + | None + | Some "n" + | Some "normal" -> initLogger VerbosityLevel.Normal + | Some "d" + | Some "detailed" -> initLogger VerbosityLevel.Detailed + | Some _ -> + elog "Invalid verbosity level" + exit 1 + + AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> closeAndFlushLog ()) + let fileToFile (force: bool) (inFile: string) (outFile: string) = - try - printfn $"Processing %s{inFile}" - let hasByteOrderMark = hasByteOrderMark inFile + async { + logGrEqDetailed $"Processing %s{inFile}" + let! hasByteOrderMark = hasByteOrderMark inFile use buffer = if hasByteOrderMark then @@ -254,44 +289,37 @@ let main argv = else new StreamWriter(outFile) - if profile then - File.ReadLines(inFile) |> Seq.length |> printfn "Line count: %i" + let! processResult = processSourceFile force profile inFile buffer - time (fun () -> processSourceFile force inFile buffer) - else - processSourceFile force inFile buffer - - buffer.Flush() - printfn "%s has been written." outFile - with exn -> - reraise () + do! buffer.FlushAsync() |> Async.AwaitTask + logGrEqDetailed $"%s{outFile} has been written." + return processResult + } let stringToFile (force: bool) (s: string) (outFile: string) config = - try - if profile then - printfn "Line count: %i" (s.Length - s.Replace(Environment.NewLine, "").Length) - - time (fun () -> processSourceString force s outFile config) - else - processSourceString force s outFile config - with exn -> - reraise () + async { return! processSourceString force profile s outFile config } let processFile force inputFile outputFile = - if inputFile <> outputFile then - fileToFile force inputFile outputFile - else - printfn "Processing %s" inputFile - let content = File.ReadAllText inputFile - let config = EditorConfig.readConfiguration inputFile - stringToFile force content inputFile config + async { + try + if inputFile <> outputFile then + return! fileToFile force inputFile outputFile + else + logGrEqDetailed $"Processing %s{inputFile}" + let! content = File.ReadAllTextAsync inputFile |> Async.AwaitTask + let config = EditorConfig.readConfiguration inputFile + return! stringToFile force content inputFile config + with e -> + return FormatResult.Error(inputFile, e) + } let processFolder force inputFolder outputFolder = if not <| Directory.Exists(outputFolder) then Directory.CreateDirectory(outputFolder) |> ignore - allFiles recurse inputFolder - |> Seq.iter (fun i -> + findAllFilesRecursively inputFolder + |> Seq.toList + |> List.map (fun i -> // s supposes to have form s1/suffix let suffix = i.Substring(inputFolder.Length + 1) @@ -303,22 +331,125 @@ let main argv = processFile force i o) - let filesAndFolders force (files: string list) (folders: string list) : unit = - files - |> List.iter (fun file -> - if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then - printfn "'%s' was ignored" file - else - processFile force file file) + let filesAndFolders force (files: string list) (folders: string list) = + let fileTasks = + files + |> List.map (fun file -> + if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then + logGrEqDetailed $"'%s{file}' was ignored" + async.Return(FormatResult.IgnoredFile(file)) + else + processFile force file file) - folders |> List.iter (fun folder -> processFolder force folder folder) + let folderTasks = + folders |> List.collect (fun folder -> processFolder force folder folder) + + (fileTasks @ folderTasks) let check = results.Contains <@ Arguments.Check @> let isDaemon = results.Contains <@ Arguments.Daemon @> + let partitionResults (results: #seq) = + (([], [], [], []), results) + ||> Seq.fold (fun (oks, ignores, unchanged, errors) next -> + match next with + | FormatResult.Formatted(file, _, p) -> ((file, p) :: oks, ignores, unchanged, errors) + | FormatResult.IgnoredFile i -> (oks, i :: ignores, unchanged, errors) + | FormatResult.Unchanged(file, p) -> (oks, ignores, (file, p) :: unchanged, errors) + | FormatResult.Error(file, e) -> (oks, ignores, unchanged, (file, e) :: errors) + | FormatResult.InvalidCode(file, _) -> + let ex = invalidResultException file + (oks, ignores, unchanged, (file, ex) :: errors)) + + let reportFormatResults (results: #seq) = + let reportError (file, exn: Exception) = + let message = + match verbosity with + | VerbosityLevel.Normal -> + match exn with + | :? ParseException -> "Could not parse file." + | :? FormatException as fe -> fe.Message + | _ -> "" + | VerbosityLevel.Detailed -> $"%A{exn}" + + let message = + if String.IsNullOrEmpty message then + message + else + $" : {message}" + + elog $"Failed to format file: {file}{message}" + + let reportProfileInfos (results: (string * ProfileInfo option) list) = + if profile && not (List.isEmpty results) then + let table = Table().AddColumns([| "File"; "Line count"; "Time taken" |]) + + results + |> List.choose (fun (f, p) -> p |> Option.map (fun p -> f, p)) + |> List.sortBy fst + |> List.fold + (fun (t: Table) (f, p) -> + t.AddRow([| f; string p.LineCount; p.TimeTaken.ToString("mm\:ss\.fff") |])) + table + |> AnsiConsole.Write + + match Seq.tryExactlyOne results with + | Some singleResult -> + let fileName f = FileInfo(f).Name + + let reportProfileInfo (f, p: ProfileInfo option) = + match profile, p with + | true, Some pI -> stdlog $"%s{f} Line count: %d{pI.LineCount} Time taken {pI.TimeTaken}" + | _ -> () + + match singleResult with + | FormatResult.Formatted(f, _, p) -> + stdlog $"{fileName f} was formatted." + reportProfileInfo (f, p) + | FormatResult.IgnoredFile f -> stdlog $"{fileName f} was ignored." + | FormatResult.Unchanged(f, p) -> + stdlog $"{fileName f} was unchanged." + reportProfileInfo (f, p) + | FormatResult.Error(f, e) -> + reportError (fileName f, e) + exit 1 + | FormatResult.InvalidCode(f, _) -> + let ex = invalidResultException f + reportError (fileName f, ex) + exit 1 + + | None -> + let oks, ignored, unchanged, errored = partitionResults results + let centeredColumn (v: string) = TableColumn(v).Centered() + + Table() + .AddColumns( + [| "[green]Formatted[/]" + string oks.Length + "Ignored" + string ignored.Length + "[blue]Unchanged[/]" + string unchanged.Length + "[red]Errored[/]" + string errored.Length |] + |> Array.map centeredColumn + ) + .SetBorder(TableBorder.MinimalDoubleHead) + |> AnsiConsole.Write + + for e in errored do + reportError e + + reportProfileInfos (oks @ unchanged) + + if errored.Length > 0 then + exit 1 + + let asyncRunner = Async.Parallel >> Async.RunSynchronously + if Option.isSome version then let version = CodeFormatter.GetVersion() - printfn $"Fantomas v%s{version}" + stdlog $"Fantomas v%s{version}" elif isDaemon then let daemon = new FantomasDaemon(Console.OpenStandardOutput(), Console.OpenStandardInput()) @@ -328,31 +459,35 @@ let main argv = daemon.WaitForClose.GetAwaiter().GetResult() exit 0 elif check then - inputPath |> runCheckCommand recurse |> exit + inputPath |> runCheckCommand |> exit else try match inputPath, outputPath with | InputPath.NoFSharpFile s, _ -> - eprintfn "Input path '%s' is unsupported file type." s + elog $"Input path '%s{s}' is unsupported file type." exit 1 | InputPath.NotFound s, _ -> - eprintfn "Input path '%s' not found." s + elog $"Input path '%s{s}' not found." exit 1 | InputPath.Unspecified, _ -> - eprintfn "Input path is missing. Call with --help for usage information." + elog "Input path is missing. Call with --help for usage information." exit 1 | InputPath.File f, _ when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> - printfn "'%s' was ignored" f - | InputPath.Folder p1, OutputPath.NotKnown -> processFolder force p1 p1 - | InputPath.File p1, OutputPath.NotKnown -> processFile force p1 p1 - | InputPath.File p1, OutputPath.IO p2 -> processFile force p1 p2 - | InputPath.Folder p1, OutputPath.IO p2 -> processFolder force p1 p2 - | InputPath.Multiple(files, folders), OutputPath.NotKnown -> filesAndFolders force files folders + logGrEqDetailed $"'%s{f}' was ignored" + | InputPath.Folder p1, OutputPath.NotKnown -> + processFolder force p1 p1 |> asyncRunner |> reportFormatResults + | InputPath.File p1, OutputPath.NotKnown -> + processFile force p1 p1 |> List.singleton |> asyncRunner |> reportFormatResults + | InputPath.File p1, OutputPath.IO p2 -> + processFile force p1 p2 |> List.singleton |> asyncRunner |> reportFormatResults + | InputPath.Folder p1, OutputPath.IO p2 -> processFolder force p1 p2 |> asyncRunner |> reportFormatResults + | InputPath.Multiple(files, folders), OutputPath.NotKnown -> + filesAndFolders force files folders |> asyncRunner |> reportFormatResults | InputPath.Multiple _, OutputPath.IO _ -> - eprintfn "Multiple input files are not supported with the --out flag." + elog "Multiple input files are not supported with the --out flag." exit 1 with exn -> - printfn "%s" exn.Message + elog $"%s{exn.Message}" exit 1 0 diff --git a/src/Fantomas/packages.lock.json b/src/Fantomas/packages.lock.json index 8df4e48d7c..5342a0e51f 100644 --- a/src/Fantomas/packages.lock.json +++ b/src/Fantomas/packages.lock.json @@ -48,6 +48,21 @@ "resolved": "0.1.8", "contentHash": "hHUZIVz9BlF++B5w183c5HwbqSIXUtJU+lxhKz3ebQ5X8INBIWV7dS/FK8uSqSMUTYavuKkRRTZvJlbYXPUykg==" }, + "Serilog": { + "type": "Direct", + "requested": "[2.12.0, )", + "resolved": "2.12.0", + "contentHash": "xaiJLIdu6rYMKfQMYUZgTy8YK7SMZjB4Yk50C/u//Z4OsvxkUfSPJy4nknfvwAC34yr13q7kcyh4grbwhSxyZg==" + }, + "Serilog.Sinks.Console": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "K6N5q+5fetjnJPvCmkWOpJ/V8IEIoMIB1s86OzBrbxwTyHxdx3pmz4H+8+O/Dc/ftUX12DM1aynx/dDowkwzqg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, "SerilogTraceListener": { "type": "Direct", "requested": "[3.2.1-dev-00011, )", @@ -57,6 +72,15 @@ "Serilog": "2.8.0" } }, + "Spectre.Console": { + "type": "Direct", + "requested": "[0.46.1-preview.0.6, )", + "resolved": "0.46.1-preview.0.6", + "contentHash": "hJRBORvRHxxD3SjhnV7h0E6SY22iJVoP7oLtKz/YhVlNarMVOWe62qjQrk6+IF8M4D16Y+PC+D7C4W1rRLUCIg==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "StreamJsonRpc": { "type": "Direct", "requested": "[2.8.28, )", @@ -347,14 +371,6 @@ "resolved": "2.0.2", "contentHash": "4EQgYdNZ92SyaO7YFk6olVnebF5V+jrHyMUjvPq89tLeMo8NSfgDF+6Zwq/lgh9j/0yfQp9Lkm0ZA0rUATCZFA==" }, - "Serilog": { - "type": "Transitive", - "resolved": "2.8.0", - "contentHash": "zjuKXW5IQws43IHX7VY9nURsaCiBYh2kyJCWLJRSWrTsx/syBKHV8MibWe2A+QH3Er0AiwA+OJmO3DhFJDY1+A==", - "dependencies": { - "System.Collections.NonGeneric": "4.3.0" - } - }, "System.Collections": { "type": "Transitive", "resolved": "4.3.0", @@ -387,19 +403,6 @@ "resolved": "5.0.0", "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==" }, - "System.Collections.NonGeneric": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "4.4.0", diff --git a/tests/regressions.fsx b/tests/regressions.fsx index bfcf2e48b8..7c028db97f 100644 --- a/tests/regressions.fsx +++ b/tests/regressions.fsx @@ -43,7 +43,7 @@ let git (workingDirectory: string) (arguments: string) = wrap workingDirectory " let format (workingDirectory: string) (input: string list) = let input = String.concat " " input - wrap workingDirectory "dotnet" $"{fantomasBinary} {input} --recurse" + wrap workingDirectory "dotnet" $"{fantomasBinary} {input}" let runCommands (workingDirectory: string) (commands: Command list) = task {