From ea60b2278c17951527d32a98a6071827a06a5179 Mon Sep 17 00:00:00 2001 From: dawe Date: Sun, 15 Jan 2023 15:25:15 +0100 Subject: [PATCH 01/34] Extend logging and verbosity options (#2693) * First stab at logging. --verbosity d should be the same as in the past --verbosity n should be pretty quiet * Make use of Serilog * improve test specificity * add log messages about count of processed files * Use interpolated strings for all logger functions. Change some logs to be error logs. --- src/Fantomas.Tests/Integration/ConfigTests.fs | 14 +- src/Fantomas.Tests/Integration/ForceTests.fs | 5 +- .../Integration/IgnoreFilesTests.fs | 22 +-- .../Integration/MultiplePathsTests.fs | 5 +- src/Fantomas.Tests/Integration/WriteTests.fs | 11 +- src/Fantomas.Tests/packages.lock.json | 26 ++-- src/Fantomas/Fantomas.fsproj | 3 + src/Fantomas/IgnoreFile.fs | 3 +- src/Fantomas/Logging.fs | 27 ++++ src/Fantomas/Program.fs | 136 +++++++++++------- src/Fantomas/packages.lock.json | 36 ++--- 11 files changed, 183 insertions(+), 105 deletions(-) create mode 100644 src/Fantomas/Logging.fs diff --git a/src/Fantomas.Tests/Integration/ConfigTests.fs b/src/Fantomas.Tests/Integration/ConfigTests.fs index c931ec3463..27fcaa9933 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" NormalVerbosity 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) 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..5c5a1fbd6f 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" @@ -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 (sprintf "%s .%c%s" Verbosity Path.DirectorySeparatorChar subFolder) exitCode |> should equal 0 File.ReadAllText inputFixture.Filename |> should equal Source + output |> should contain "Processed files: 0" + [] 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 "Processed files: 1" diff --git a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs index 2b2cdec182..2806389c19 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) @@ -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\" -r" 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..398366087f 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": { @@ -397,19 +402,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,6 +913,8 @@ "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, )", "StreamJsonRpc": "[2.8.28, )", "System.IO.Abstractions": "[17.2.3, )", diff --git a/src/Fantomas/Fantomas.fsproj b/src/Fantomas/Fantomas.fsproj index 9b5c233e04..638b316507 100644 --- a/src/Fantomas/Fantomas.fsproj +++ b/src/Fantomas/Fantomas.fsproj @@ -20,6 +20,7 @@ + @@ -33,6 +34,8 @@ + + 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..5ff6d9ea11 --- /dev/null +++ b/src/Fantomas/Logging.fs @@ -0,0 +1,27 @@ +module Fantomas.Logging + +open Serilog + +[] +type VerbosityLevel = + | Normal + | Detailed + +let private logger = + Log.Logger <- LoggerConfiguration().WriteTo.Console().CreateLogger() + 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 verbosity s = + if verbosity = VerbosityLevel.Detailed then + logger.Information(s) + else + () + +let closeAndFlushLog () = Log.CloseAndFlush() diff --git a/src/Fantomas/Program.fs b/src/Fantomas/Program.fs index f45a8de250..83d950d1da 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -3,6 +3,7 @@ open System.IO open Fantomas.Core open Fantomas open Fantomas.Daemon +open Fantomas.Logging open Argu open System.Text open Fantomas.Format @@ -17,6 +18,7 @@ type Arguments = | [] Check | [] Daemon | [] Version + | [] Verbosity of string | [] Input of string list interface IArgParserTemplate with @@ -35,12 +37,13 @@ type Arguments = sprintf "Input paths: can be multiple folders or files with %s extension." (Seq.map (fun s -> "*" + s) extensions |> String.concat ",") + | Verbosity _ -> "Set the verbosity level. Allowed values are n[ormal] and d[etailed]." let time f = let sw = Diagnostics.Stopwatch.StartNew() let res = f () sw.Stop() - printfn "Time taken: %O s" sw.Elapsed + stdlog $"Time taken: %O{sw.Elapsed} s" res [] @@ -94,14 +97,14 @@ let private hasByteOrderMark file = false /// Format a source string using given config and write to a text writer -let processSourceString (force: bool) s (fileName: string) config = +let processSourceString verbosity (force: bool) s (fileName: string) config = let writeResult (formatted: string) = if hasByteOrderMark fileName then File.WriteAllText(fileName, formatted, Encoding.UTF8) else File.WriteAllText(fileName, formatted) - printfn $"%s{fileName} has been written." + logGrEqDetailed verbosity $"%s{fileName} has been written." async { let! formatted = s |> Format.formatContentAsync config fileName @@ -109,65 +112,65 @@ let processSourceString (force: bool) s (fileName: string) config = match formatted with | Format.FormatResult.Formatted(_, formattedContent) -> formattedContent |> writeResult | Format.InvalidCode(file, formattedContent) when force -> - printfn $"%s{file} was not valid after formatting." + stdlog $"%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.Unchanged file -> logGrEqDetailed verbosity $"'%s{file}' was unchanged" + | Format.IgnoredFile file -> logGrEqDetailed verbosity $"'%s{file}' was ignored" | Format.FormatResult.Error(_, ex) -> raise ex | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") } |> Async.RunSynchronously /// Format inFile and write to text writer -let processSourceFile (force: bool) inFile (tw: TextWriter) = +let processSourceFile verbosity (force: bool) inFile (tw: TextWriter) = async { let! formatted = Format.formatFileAsync inFile match formatted with | Format.FormatResult.Formatted(_, formattedContent) -> tw.Write(formattedContent) | Format.InvalidCode(file, formattedContent) when force -> - printfn $"%s{file} was not valid after formatting." + stdlog $"%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.IgnoredFile file -> logGrEqDetailed verbosity $"'%s{file}' was ignored" | Format.FormatResult.Error(_, ex) -> raise ex | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") } |> Async.RunSynchronously -let private reportCheckResults (output: TextWriter) (checkResult: Format.CheckResult) = +let private reportCheckResults (checkResult: Format.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 (verbosity: VerbosityLevel) (recurse: bool) (inputPath: InputPath) : int = let check files = Async.RunSynchronously(Format.checkCode files) let processCheckResult (checkResult: Format.CheckResult) = if checkResult.IsValid then - stdout.WriteLine "No changes required." + logGrEqDetailed verbosity "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 verbosity $"'%s{f}' was ignored" 0 | InputPath.File path -> path |> Seq.singleton |> check |> processCheckResult | InputPath.Folder path -> path |> allFiles recurse |> check |> processCheckResult @@ -240,9 +243,26 @@ let main argv = let version = results.TryGetResult <@ Arguments.Version @> + let verbosity = + let maybeVerbosity = + results.TryGetResult <@ Arguments.Verbosity @> + |> Option.map (fun v -> v.ToLowerInvariant()) + + match maybeVerbosity with + | None + | Some "n" + | Some "normal" -> VerbosityLevel.Normal + | Some "d" + | Some "detailed" -> 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}" + logGrEqDetailed verbosity $"Processing %s{inFile}" let hasByteOrderMark = hasByteOrderMark inFile use buffer = @@ -255,25 +275,25 @@ let main argv = new StreamWriter(outFile) if profile then - File.ReadLines(inFile) |> Seq.length |> printfn "Line count: %i" + File.ReadLines(inFile) |> Seq.length |> (fun l -> stdlog $"Line count: %i{l}") - time (fun () -> processSourceFile force inFile buffer) + time (fun () -> processSourceFile verbosity force inFile buffer) else - processSourceFile force inFile buffer + processSourceFile verbosity force inFile buffer buffer.Flush() - printfn "%s has been written." outFile + logGrEqDetailed verbosity $"%s{outFile} has been written." with exn -> reraise () let stringToFile (force: bool) (s: string) (outFile: string) config = try if profile then - printfn "Line count: %i" (s.Length - s.Replace(Environment.NewLine, "").Length) + stdlog $"""Line count: %i{s.Length - s.Replace(Environment.NewLine, "").Length}""" - time (fun () -> processSourceString force s outFile config) + time (fun () -> processSourceString verbosity force s outFile config) else - processSourceString force s outFile config + processSourceString verbosity force s outFile config with exn -> reraise () @@ -281,7 +301,7 @@ let main argv = if inputFile <> outputFile then fileToFile force inputFile outputFile else - printfn "Processing %s" inputFile + logGrEqDetailed verbosity $"Processing %s{inputFile}" let content = File.ReadAllText inputFile let config = EditorConfig.readConfiguration inputFile stringToFile force content inputFile config @@ -290,7 +310,9 @@ let main argv = if not <| Directory.Exists(outputFolder) then Directory.CreateDirectory(outputFolder) |> ignore - allFiles recurse inputFolder + let files = allFiles recurse inputFolder + + files |> Seq.iter (fun i -> // s supposes to have form s1/suffix let suffix = i.Substring(inputFolder.Length + 1) @@ -303,22 +325,30 @@ 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) + Seq.length files + + let filesAndFolders force (files: string list) (folders: string list) : int = + let singleFilesProcessed = + files + |> List.sumBy (fun file -> + if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then + logGrEqDetailed verbosity $"'%s{file}' was ignored" + 0 + else + processFile force file file + 1) + + let filesInFoldersProcessed = + folders |> List.sumBy (fun folder -> processFolder force folder folder) - folders |> List.iter (fun folder -> processFolder force folder folder) + singleFilesProcessed + filesInFoldersProcessed let check = results.Contains <@ Arguments.Check @> let isDaemon = results.Contains <@ Arguments.Daemon @> 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 +358,37 @@ let main argv = daemon.WaitForClose.GetAwaiter().GetResult() exit 0 elif check then - inputPath |> runCheckCommand recurse |> exit + inputPath |> runCheckCommand verbosity recurse |> 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 + logGrEqDetailed verbosity $"'%s{f}' was ignored" + | InputPath.Folder p1, OutputPath.NotKnown -> + let n = processFolder force p1 p1 + logGrEqDetailed verbosity $"Processed files: %d{n}" | 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 + | InputPath.Folder p1, OutputPath.IO p2 -> + let n = processFolder force p1 p2 + logGrEqDetailed verbosity $"Processed files: %d{n}" + | InputPath.Multiple(files, folders), OutputPath.NotKnown -> + let n = filesAndFolders force files folders + logGrEqDetailed verbosity $"Processed files: %d{n}" | 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..168879c6be 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, )", @@ -347,14 +362,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 +394,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", From e1c0926dfacc662f3b492c0f01ab923c0f37e4ea Mon Sep 17 00:00:00 2001 From: dawe Date: Fri, 20 Jan 2023 09:02:50 +0100 Subject: [PATCH 02/34] Sunset MultilineBlockBracketsOnSameColumn & ExperimentalStroustrupStyle (#2728) * Extend logging and verbosity options (#2693) * First stab at logging. --verbosity d should be the same as in the past --verbosity n should be pretty quiet * Make use of Serilog * improve test specificity * add log messages about count of processed files * Use interpolated strings for all logger functions. Change some logs to be error logs. * remove MultilineBlockBracketsOnSameColumn & ExperimentalStroustrupStyle from src/ * remove MultilineBlockBracketsOnSameColumn & ExperimentalStroustrupStyle from Rider.md * remove MultilineBlockBracketsOnSameColumn & ExperimentalStroustrupStyle from Configuration.fsx * remove orange (use with caution) marker from fsharp_multiline_bracket_style --- docs/docs/end-users/Configuration.fsx | 84 ++++++++++--------- docs/docs/end-users/Rider.md | 3 +- .../EditorConfigurationTests.fs | 39 --------- src/Fantomas/EditorConfig.fs | 31 +------ 4 files changed, 48 insertions(+), 109 deletions(-) diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index a4af68e244..d36fa21e6d 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -39,7 +39,7 @@ You can quickly try your settings via the *) -#r "nuget: Fantomas.Core, 5.*" +#r "nuget: Fantomas.Core, 5.2.0-alpha-012" open Fantomas.Core.FormatConfig open Fantomas.Core @@ -55,7 +55,7 @@ However, there are settings that we do not recommend and generally should not be

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.

+

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

*) (** @@ -63,7 +63,7 @@ However, there are settings that we do not recommend and generally should not be -`indent_size` has to be between 1 and 10. +` indent_size` has to be between 1 and 10. This preference sets the indentation The common values are 2 and 4. @@ -572,56 +572,64 @@ formatCode (*** include-it ***) (** - + -How to format bracketted expressions (e.g. records, arrays, lists, etc.) that span multiple lines. +`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. +`ExperimentalStroustrup` 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). -_This setting replaces the deprecated settings `fsharp_multiline_block_brackets_on_same_column` and `fsharp_experimental_stroustrup_style`._ - -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" ] - """ - { FormatConfig.Default with - MultilineBracketStyle = Cramped } -(*** include-it ***) + let myRecord = + { Level = 1 + Progress = "foo" + Bar = "bar" + Street = "Bakerstreet" + Number = 42 } -(** -**Aligned** - Alternative way of formatting brackets. This will align the braces at the same column level. -*) + type Range = + { From: float + To: float + FileName: string } -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 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 ***) -(** -**ExperimentalStroustrup** - Experimental setting. Places the opening brace on the same line as the binding, and the closing brace on its own line. - -_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)._ -*) - 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 = ExperimentalStroustrup } 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/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 962fd81924..eda9023e31 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -448,26 +448,6 @@ insert_final_newline = false Assert.IsFalse config.InsertFinalNewline -[] -let ``fsharp_experimental_stroustrup_style = true`` () = - let rootDir = tempName () - - let editorConfig = - """ -[*.fs] -fsharp_multiline_block_brackets_on_same_column = true -fsharp_experimental_stroustrup_style = true -""" - - use configFixture = - new ConfigurationFile(defaultConfig, rootDir, content = editorConfig) - - use fsharpFile = new FSharpFile(rootDir) - - let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - - Assert.AreEqual(ExperimentalStroustrup, config.MultilineBracketStyle) - [] let ``fsharp_multiline_bracket_style = experimental_stroustrup`` () = let rootDir = tempName () @@ -524,22 +504,3 @@ fsharp_multiline_bracket_style = cramped let config = EditorConfig.readConfiguration fsharpFile.FSharpFile Assert.AreEqual(Cramped, config.MultilineBracketStyle) - -[] -let ``fsharp_multiline_block_brackets_on_same_column = true`` () = - let rootDir = tempName () - - let editorConfig = - """ -[*.fs] -fsharp_multiline_block_brackets_on_same_column = true -""" - - use configFixture = - new ConfigurationFile(defaultConfig, rootDir, content = editorConfig) - - use fsharpFile = new FSharpFile(rootDir) - - let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - - Assert.AreEqual(Aligned, config.MultilineBracketStyle) diff --git a/src/Fantomas/EditorConfig.fs b/src/Fantomas/EditorConfig.fs index 41870ece44..39e6f4826c 100644 --- a/src/Fantomas/EditorConfig.fs +++ b/src/Fantomas/EditorConfig.fs @@ -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 From 240b1351525ab161904fd415e49f49ffaa604135 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 20 Jan 2023 09:03:16 +0100 Subject: [PATCH 03/34] Move FormatConfig into Fantomas.Core namespace. (#2736) * Move FormatConfig into Fantomas.Core namespace. * Remove unused opens. --- src/Fantomas.Benchmarks/Runners.fs | 2 +- src/Fantomas.Core.Tests/ASTTransformerTests.fs | 1 - .../CodePrinterHelperFunctionsTests.fs | 1 - src/Fantomas.Core.Tests/ColMultilineItemTests.fs | 2 +- src/Fantomas.Core.Tests/CommentTests.fs | 2 +- .../ComputationExpressionTests.fs | 2 +- src/Fantomas.Core.Tests/ContextTests.fs | 1 - src/Fantomas.Core.Tests/ControlStructureTests.fs | 2 +- src/Fantomas.Core.Tests/DallasTests.fs | 2 +- src/Fantomas.Core.Tests/DotGetTests.fs | 2 +- src/Fantomas.Core.Tests/DotIndexedGetTests.fs | 2 +- .../FormattingSelectionOnlyTests.fs | 2 +- src/Fantomas.Core.Tests/InterpolatedStringTests.fs | 1 - src/Fantomas.Core.Tests/KeepIndentInBranchTests.fs | 2 +- src/Fantomas.Core.Tests/KeepMaxEmptyLinesTests.fs | 2 +- src/Fantomas.Core.Tests/LambdaTests.fs | 2 +- src/Fantomas.Core.Tests/LetBindingTests.fs | 2 +- src/Fantomas.Core.Tests/ListTests.fs | 2 +- .../MultiLineLambdaClosingNewlineTests.fs | 2 +- ...ilineBlockBracketsOnSameColumnArrayOrListTests.fs | 2 +- .../MultilineBlockBracketsOnSameColumnRecordTests.fs | 2 +- .../NumberOfItemsListOrArrayTests.fs | 2 +- src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs | 2 +- src/Fantomas.Core.Tests/OpenTypeTests.fs | 1 - src/Fantomas.Core.Tests/OperatorTests.fs | 2 +- src/Fantomas.Core.Tests/RecordTests.fs | 2 +- src/Fantomas.Core.Tests/SignatureTests.fs | 2 +- .../SpaceBeforeClassConstructorTests.fs | 2 +- .../Stroustrup/DotIndexedSetExpressionTests.fs | 2 +- .../Stroustrup/DotSetExpressionTests.fs | 2 +- src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs | 4 ++-- .../Stroustrup/FunctionApplicationDualListTests.fs | 2 +- .../Stroustrup/FunctionApplicationSingleListTests.fs | 2 +- .../Stroustrup/KeepIndentInBranchExpressionTests.fs | 2 +- .../Stroustrup/LambdaExpressionTests.fs | 2 +- .../Stroustrup/LetOrUseBangExpressionTests.fs | 2 +- .../Stroustrup/LongIdentSetExpressionTests.fs | 2 +- .../MultiLineLambdaClosingNewlineExpressionTests.fs | 2 +- .../Stroustrup/NamedArgumentExpressionTests.fs | 2 +- .../Stroustrup/SetExpressionTests.fs | 2 +- .../Stroustrup/SynBindingFunctionExpressionTests.fs | 2 +- .../SynBindingFunctionLongPatternExpressionTests.fs | 2 +- ...ynBindingFunctionWithReturnTypeExpressionTests.fs | 2 +- .../Stroustrup/SynBindingValueExpressionTests.fs | 2 +- .../Stroustrup/SynExprAndBangExpressionTests.fs | 2 +- .../Stroustrup/SynMatchClauseExpressionTests.fs | 2 +- .../Stroustrup/SynTypeDefnSigReprSimpleTests.fs | 2 +- .../Stroustrup/SynTypeDefnSimpleReprRecordTests.fs | 2 +- .../Stroustrup/YieldOrReturnBangExpressionTests.fs | 2 +- .../Stroustrup/YieldOrReturnExpressionTests.fs | 2 +- src/Fantomas.Core.Tests/TestHelpers.fs | 3 +-- src/Fantomas.Core.Tests/TypeDeclarationTests.fs | 2 +- src/Fantomas.Core.Tests/TypeProviderTests.fs | 2 +- src/Fantomas.Core.Tests/UnionTests.fs | 2 +- src/Fantomas.Core/CodeFormatter.fs | 6 +++--- src/Fantomas.Core/CodeFormatter.fsi | 1 - src/Fantomas.Core/CodeFormatterImpl.fs | 2 -- src/Fantomas.Core/CodeFormatterImpl.fsi | 1 - src/Fantomas.Core/CodePrinter.fs | 1 - src/Fantomas.Core/Context.fs | 1 - src/Fantomas.Core/Context.fsi | 2 -- src/Fantomas.Core/FormatConfig.fs | 2 +- src/Fantomas.Core/Selection.fs | 2 -- src/Fantomas.Core/Selection.fsi | 1 - src/Fantomas.Core/Trivia.fs | 1 - src/Fantomas.Core/Trivia.fsi | 1 - src/Fantomas.Tests/EditorConfigurationTests.fs | 3 +-- src/Fantomas.Tests/Integration/ConfigTests.fs | 6 ++---- src/Fantomas.Tests/Integration/DaemonTests.fs | 2 +- src/Fantomas/Daemon.fs | 3 +-- src/Fantomas/EditorConfig.fs | 2 +- src/Fantomas/EditorConfig.fsi | 12 +++++++----- src/Fantomas/Format.fs | 1 - src/Fantomas/Format.fsi | 3 ++- 74 files changed, 69 insertions(+), 90 deletions(-) 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.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/CodePrinterHelperFunctionsTests.fs b/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs index 3e127072ac..4d3b973983 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 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..a9c86c1b84 100644 --- a/src/Fantomas.Core.Tests/ContextTests.fs +++ b/src/Fantomas.Core.Tests/ContextTests.fs @@ -4,7 +4,6 @@ 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 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/DallasTests.fs b/src/Fantomas.Core.Tests/DallasTests.fs index ed5e27e7a7..039a54707c 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`` () = 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/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..c5f333c88b 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 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/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/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs b/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs index 64bc6f0bc3..c5466a5f65 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs +++ b/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnArrayOrListTests 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/MultilineBlockBracketsOnSameColumnRecordTests.fs index 8d89f76448..b05ceaf2eb 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs +++ b/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnRecordTests 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/NumberOfItemsListOrArrayTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs index 628abb822e..44a00d904e 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`` () = diff --git a/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs index b882e1f54c..730d6e4135 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 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/RecordTests.fs b/src/Fantomas.Core.Tests/RecordTests.fs index 675fcbf368..583bb69c93 100644 --- a/src/Fantomas.Core.Tests/RecordTests.fs +++ b/src/Fantomas.Core.Tests/RecordTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.RecordTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core [] let ``record declaration`` () = 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/Stroustrup/DotIndexedSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs index 88d69a4e49..66d33f056a 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.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/Stroustrup/DotSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs index e345eb9a6e..7e13318430 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.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/Stroustrup/ElmishTests.fs b/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs index 22d60553c3..9272a1064e 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs @@ -3,7 +3,7 @@ module Fantomas.Core.Tests.ElmishTests open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core let config = { config with @@ -1355,7 +1355,7 @@ let Dashboard () = ] """ { config with - RecordMultilineFormatter = Fantomas.Core.FormatConfig.MultilineFormatterType.NumberOfItems + RecordMultilineFormatter = MultilineFormatterType.NumberOfItems MaxArrayOrListWidth = 20 // MaxElmishWidth = 10 MultiLineLambdaClosingNewline = true } diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs index 5bc637a864..b2fc44e629 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.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/Stroustrup/FunctionApplicationSingleListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs index a100c73de5..4da71aaef8 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.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/Stroustrup/KeepIndentInBranchExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs index 986252a46e..e1bd30ccc1 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs @@ -3,7 +3,7 @@ open NUnit.Framework open FsUnit open Fantomas.Core.Tests.TestHelper -open Fantomas.Core.FormatConfig +open Fantomas.Core // ExperimentalKeepIndentInBranch has precedence over ExperimentalStroustrupStyle diff --git a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs index 74b944bd77..ab6914fd5b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.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/Stroustrup/LetOrUseBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs index e04c7b04ac..f095473fab 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs index f230f6db86..208456d3a0 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs index 8540d0ca21..4705fe4e85 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.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/Stroustrup/NamedArgumentExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs index 716a642543..16a88577fa 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.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/Stroustrup/SetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs index 6ad063f916..9aa5d0b4cb 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.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/Stroustrup/SynBindingFunctionExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs index f48d866057..1208f15155 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs index b47c444e4a..42565d873a 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs index 9b2f224d53..d164bf3648 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs index 84e4040459..e368e62ecb 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.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/Stroustrup/SynExprAndBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs index b330cf93b5..bd5b52a0a9 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs index 153490a445..6502c475c8 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.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/Stroustrup/SynTypeDefnSigReprSimpleTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs index 12058bbaa9..2f4698f006 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.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/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs index 11e531eab4..8c86841863 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.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/Stroustrup/YieldOrReturnBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs index e23c3b8df8..47473ca144 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs index 6828057ab7..8c2b46b72b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs @@ -2,7 +2,7 @@ open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig +open Fantomas.Core open Fantomas.Core.Tests.TestHelper let config = diff --git a/src/Fantomas.Core.Tests/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index cc35bd77df..cb1eeedd2d 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -1,11 +1,10 @@ module Fantomas.Core.Tests.TestHelper open System +open Fantomas.Core open Fantomas.Core.SyntaxOak open NUnit.Framework open FsUnit -open Fantomas.Core.FormatConfig -open Fantomas.Core [] do () 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..7735df89b4 100644 --- a/src/Fantomas.Core.Tests/TypeProviderTests.fs +++ b/src/Fantomas.Core.Tests/TypeProviderTests.fs @@ -49,7 +49,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..c3d2c99be5 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`` () = diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 83573c0a31..0ac2bbacbc 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -10,18 +10,18 @@ type CodeFormatter = static member FormatASTAsync(ast: ParsedInput, ?source, ?config) : Async = let sourceText = Option.map CodeFormatterImpl.getSourceText source - let config = Option.defaultValue FormatConfig.FormatConfig.Default config + let config = Option.defaultValue FormatConfig.Default config CodeFormatterImpl.formatAST ast sourceText config |> async.Return static member FormatDocumentAsync(isSignature, source, config) = - let config = Option.defaultValue FormatConfig.FormatConfig.Default config + let config = Option.defaultValue FormatConfig.Default config CodeFormatterImpl.getSourceText source |> CodeFormatterImpl.formatDocument config isSignature static member FormatSelectionAsync(isSignature, source, selection, config) = - let config = Option.defaultValue FormatConfig.FormatConfig.Default config + let config = Option.defaultValue FormatConfig.Default config CodeFormatterImpl.getSourceText source |> Selection.formatSelection config isSignature selection diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index dc8e37e84a..fe69ba9e92 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -1,6 +1,5 @@ namespace Fantomas.Core -open Fantomas.Core.FormatConfig open FSharp.Compiler.Text open FSharp.Compiler.Syntax diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 15bfd0ed58..46f3c2feb4 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -4,8 +4,6 @@ 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 let getSourceText (source: string) : ISourceText = source.TrimEnd() |> SourceText.ofString diff --git a/src/Fantomas.Core/CodeFormatterImpl.fsi b/src/Fantomas.Core/CodeFormatterImpl.fsi index bbf9ea6783..1e9e815146 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fsi +++ b/src/Fantomas.Core/CodeFormatterImpl.fsi @@ -3,7 +3,6 @@ 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 diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 5ddb4f4b56..dbf17ffaa2 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 [| "="; ">"; "<"; "%" |] diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 8ddb9a2231..1908074979 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -2,7 +2,6 @@ module internal Fantomas.Core.Context open System open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas.Core.SyntaxOak type WriterEvent = diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index cf11bd354d..787b88bdba 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -1,7 +1,5 @@ module internal Fantomas.Core.Context -open Fantomas.Core -open Fantomas.Core.FormatConfig open Fantomas.Core.SyntaxOak type WriterEvent = diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index e46e4b57d5..115e0b4dea 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.FormatConfig +namespace Fantomas.Core open System open System.ComponentModel diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index d6cf69ff8c..842d36397b 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 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/Trivia.fs b/src/Fantomas.Core/Trivia.fs index 7306a413ac..5a2208a37f 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 diff --git a/src/Fantomas.Core/Trivia.fsi b/src/Fantomas.Core/Trivia.fsi index a362199b48..a1ec8c32f6 100644 --- a/src/Fantomas.Core/Trivia.fsi +++ b/src/Fantomas.Core/Trivia.fsi @@ -2,7 +2,6 @@ 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 diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index eda9023e31..45158793b0 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, diff --git a/src/Fantomas.Tests/Integration/ConfigTests.fs b/src/Fantomas.Tests/Integration/ConfigTests.fs index 27fcaa9933..72c6e1544c 100644 --- a/src/Fantomas.Tests/Integration/ConfigTests.fs +++ b/src/Fantomas.Tests/Integration/ConfigTests.fs @@ -61,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 @@ -95,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..95544e8e88 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]) diff --git a/src/Fantomas/Daemon.fs b/src/Fantomas/Daemon.fs index 3e0c43635c..9aa45fefb6 100644 --- a/src/Fantomas/Daemon.fs +++ b/src/Fantomas/Daemon.fs @@ -12,7 +12,6 @@ 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 = @@ -109,7 +108,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 39e6f4826c..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 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/Format.fs b/src/Fantomas/Format.fs index ee7e276eed..e13b63de28 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -3,7 +3,6 @@ module Fantomas.Format open System open System.IO open Fantomas.Core -open Fantomas.Core.FormatConfig exception CodeFormatException of (string * Option) array with override x.ToString() = diff --git a/src/Fantomas/Format.fsi b/src/Fantomas/Format.fsi index 9d5aa64ca0..188be113d3 100644 --- a/src/Fantomas/Format.fsi +++ b/src/Fantomas/Format.fsi @@ -1,6 +1,7 @@ module Fantomas.Format open System +open Fantomas.Core exception CodeFormatException of (string * Option) array @@ -11,7 +12,7 @@ type FormatResult = | Error of filename: string * formattingError: Exception | IgnoredFile of filename: string -val formatContentAsync: (Core.FormatConfig.FormatConfig -> string -> string -> Async) +val formatContentAsync: (FormatConfig -> string -> string -> Async) val formatFileAsync: (string -> Async) From 43d47fc034c39c8d625025c4d62348cd23995e01 Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 23 Jan 2023 09:31:53 +0100 Subject: [PATCH 04/34] Let the icons (green, orange, ...) in the explanation be rendered. (#2741) --- docs/content/webcomponents.js | 28 +++++++++++++++++---------- docs/docs/end-users/Configuration.fsx | 8 ++++---- 2 files changed, 22 insertions(+), 14 deletions(-) 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/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index d36fa21e6d..b7a2c394a1 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -52,10 +52,10 @@ let formatCode input configIndent = ## 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.

*) (** From bfaf6af28c64c6cad1d49281d4c2619ea4777e37 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Tue, 24 Jan 2023 13:53:57 +0100 Subject: [PATCH 05/34] Initial cursor (#2739) * Cursor inside node. * Add support for a floating cursor. * Add some thoughts on resolving the cursor with multiple defines. * Expose cursor API in Fantomas.Client. * Add CHANGELOG entries. --- CHANGELOG.md | 8 +++ src/Fantomas.Client/CHANGELOG.md | 3 + src/Fantomas.Client/Contracts.fs | 33 ++++----- src/Fantomas.Client/Contracts.fsi | 15 +++++ src/Fantomas.Client/LSPFantomasService.fs | 18 +++-- .../LSPFantomasServiceTypes.fs | 22 +++--- .../LSPFantomasServiceTypes.fsi | 2 +- .../CodePrinterHelperFunctionsTests.fs | 2 +- src/Fantomas.Core.Tests/ContextTests.fs | 2 +- src/Fantomas.Core.Tests/CursorTests.fs | 43 ++++++++++++ .../Fantomas.Core.Tests.fsproj | 1 + src/Fantomas.Core.Tests/FormatAstTests.fs | 1 + src/Fantomas.Core.Tests/ModuleTests.fs | 2 +- src/Fantomas.Core.Tests/SynLongIdentTests.fs | 1 + src/Fantomas.Core.Tests/TestHelpers.fs | 10 +-- src/Fantomas.Core/CodeFormatter.fs | 12 ++-- src/Fantomas.Core/CodeFormatter.fsi | 30 ++++++--- src/Fantomas.Core/CodeFormatterImpl.fs | 67 ++++++++++++------- src/Fantomas.Core/CodeFormatterImpl.fsi | 6 +- src/Fantomas.Core/CodeFormatterTypes.fs | 12 ++++ src/Fantomas.Core/CodePrinter.fs | 41 ++++++++++-- src/Fantomas.Core/Context.fs | 37 +++++----- src/Fantomas.Core/Context.fsi | 11 +-- src/Fantomas.Core/Fantomas.Core.fsproj | 1 + src/Fantomas.Core/Selection.fs | 8 ++- src/Fantomas.Core/SyntaxOak.fs | 20 +++++- src/Fantomas.Core/Trivia.fs | 13 +++- src/Fantomas.Core/Trivia.fsi | 4 ++ src/Fantomas.Tests/Integration/DaemonTests.fs | 66 ++++++++++++++---- src/Fantomas/Daemon.fs | 21 ++++-- src/Fantomas/Format.fs | 3 +- 31 files changed, 386 insertions(+), 129 deletions(-) create mode 100644 src/Fantomas.Core.Tests/CursorTests.fs create mode 100644 src/Fantomas.Core/CodeFormatterTypes.fs diff --git a/CHANGELOG.md b/CHANGELOG.md index 659f5d86b6..ebd31257ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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/src/Fantomas.Client/CHANGELOG.md b/src/Fantomas.Client/CHANGELOG.md index fcd5f26be0..0bfa8043d1 100644 --- a/src/Fantomas.Client/CHANGELOG.md +++ b/src/Fantomas.Client/CHANGELOG.md @@ -2,6 +2,9 @@ This is the changelog for the Fantomas.Client package specifically. It's distinct from that of the overall libraries and command-line tool. +## 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..f348324cdc 100644 --- a/src/Fantomas.Client/LSPFantomasService.fs +++ b/src/Fantomas.Client/LSPFantomasService.fs @@ -167,14 +167,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 +216,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>) = @@ -256,7 +260,8 @@ type LSPFantomasService() = { Code = int FantomasResponseCode.Version Content = Some t.Result FilePath = filePath - SelectedRange = None })) + SelectedRange = None + Cursor = None })) |> mapResultToResponse filePath member _.FormatDocumentAsync @@ -310,7 +315,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..bbc5ec6d8d 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fs +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fs @@ -29,42 +29,48 @@ 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) -> + | FormatDocumentResponse.Formatted(name, content, cursor) -> { Code = int FantomasResponseCode.Formatted FilePath = name Content = Some content - SelectedRange = None } + SelectedRange = None + Cursor = cursor } | FormatDocumentResponse.Unchanged name -> { Code = int FantomasResponseCode.UnChanged FilePath = name Content = None - SelectedRange = None } + SelectedRange = None + Cursor = None } | FormatDocumentResponse.Error(name, err) -> { Code = int FantomasResponseCode.Error FilePath = name Content = Some(err) - SelectedRange = None } + SelectedRange = None + Cursor = None } | FormatDocumentResponse.IgnoredFile name -> { Code = int FantomasResponseCode.Ignored FilePath = name Content = None - SelectedRange = None } + SelectedRange = None + Cursor = None } type FantomasVersion = FantomasVersion of string type FantomasExecutableFile = FantomasExecutableFile of string diff --git a/src/Fantomas.Client/LSPFantomasServiceTypes.fsi b/src/Fantomas.Client/LSPFantomasServiceTypes.fsi index 075430f02e..bf346ee3b9 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fsi +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fsi @@ -24,7 +24,7 @@ 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 diff --git a/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs b/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs index 4d3b973983..464a6e64a0 100644 --- a/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs +++ b/src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs @@ -11,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/ContextTests.fs b/src/Fantomas.Core.Tests/ContextTests.fs index a9c86c1b84..8091953711 100644 --- a/src/Fantomas.Core.Tests/ContextTests.fs +++ b/src/Fantomas.Core.Tests/ContextTests.fs @@ -6,7 +6,7 @@ open Fantomas.Core.Context open Fantomas.Core.Tests.TestHelper 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/CursorTests.fs b/src/Fantomas.Core.Tests/CursorTests.fs new file mode 100644 index 0000000000..23ce640b7b --- /dev/null +++ b/src/Fantomas.Core.Tests/CursorTests.fs @@ -0,0 +1,43 @@ +module Fantomas.Core.Tests.CursorTests + +open FSharp.Compiler.Text +open NUnit.Framework +open FsUnit +open Fantomas.Core + +let assertCursor (expectedLine: int, expectedColumn: int) (actualCursor: pos) : unit = + Assert.AreEqual(Position.mkPos expectedLine expectedColumn, actualCursor) + +[] +let ``cursor inside of a node`` () = + let source = + """ +let a = + "foobar" +""" + + let formattedResult = + CodeFormatter.FormatDocumentAsync(false, source, cursor = CodeFormatter.MakePosition(3, 8)) + |> Async.RunSynchronously + + // After formatting the let binding will be on one line + + match formattedResult.Cursor with + | None -> Assert.Fail "Expected a cursor" + | Some cursor -> assertCursor (1, 12) cursor + +[] +let ``cursor outside of a node`` () = + let source = + """ +let a = + () +""" + + let formattedResult = + CodeFormatter.FormatDocumentAsync(false, source, cursor = CodeFormatter.MakePosition(3, 7)) + |> Async.RunSynchronously + + match formattedResult.Cursor with + | None -> Assert.Fail "Expected a cursor" + | Some cursor -> assertCursor (1, 11) cursor diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index cf34cd5094..2c910cc6bb 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -121,6 +121,7 @@ +
diff --git a/src/Fantomas.Core.Tests/FormatAstTests.fs b/src/Fantomas.Core.Tests/FormatAstTests.fs index 00307214e9..be737082fd 100644 --- a/src/Fantomas.Core.Tests/FormatAstTests.fs +++ b/src/Fantomas.Core.Tests/FormatAstTests.fs @@ -15,6 +15,7 @@ let parseAndFormat sourceCode = let formattedCode = CodeFormatter.FormatASTAsync(ast, source = sourceCode) |> Async.RunSynchronously + |> fun formatResult -> formatResult.Code |> String.normalizeNewLine |> fun s -> s.TrimEnd('\n') 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/SynLongIdentTests.fs b/src/Fantomas.Core.Tests/SynLongIdentTests.fs index 9e19ddb53a..d4cfc3b519 100644 --- a/src/Fantomas.Core.Tests/SynLongIdentTests.fs +++ b/src/Fantomas.Core.Tests/SynLongIdentTests.fs @@ -405,4 +405,5 @@ let ``backticks can be added from AST only scenarios`` () = InsertFinalNewline = false } ) |> Async.RunSynchronously + |> fun result -> result.Code |> should equal "``new``" diff --git a/src/Fantomas.Core.Tests/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index cb1eeedd2d..562011c84c 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -34,12 +34,12 @@ let formatSourceString isFsiFile (s: string) config = CodeFormatter.FormatASTAsync(ast, config = config) - let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted) + let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted.Code) 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{formatted.Code}" - return formatted.Replace("\r\n", "\n") + return formatted.Code.Replace("\r\n", "\n") } |> Async.RunSynchronously @@ -58,13 +58,13 @@ let formatSourceStringWithDefines defines (s: string) config = |> Array.head |> fst - return CodeFormatterImpl.formatAST ast (Some source) config + return CodeFormatterImpl.formatAST ast (Some source) config None } |> Async.RunSynchronously // merge with itself to make #if go on beginning of line let _, fragments = - String.splitInFragments config.EndOfLine.NewLineString [ (defines, result) ] + String.splitInFragments config.EndOfLine.NewLineString [ defines, result.Code ] |> List.head String.merge fragments fragments diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 0ac2bbacbc..99d2d28cb6 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -8,17 +8,15 @@ type CodeFormatter = static member ParseAsync(isSignature, source) : Async<(ParsedInput * string list) array> = CodeFormatterImpl.getSourceText source |> CodeFormatterImpl.parse isSignature - static member FormatASTAsync(ast: ParsedInput, ?source, ?config) : Async = + static member FormatASTAsync(ast: ParsedInput, ?source, ?config) : Async = let sourceText = Option.map CodeFormatterImpl.getSourceText source let config = Option.defaultValue FormatConfig.Default config - CodeFormatterImpl.formatAST ast sourceText config |> async.Return + CodeFormatterImpl.formatAST ast sourceText config None |> async.Return - static member FormatDocumentAsync(isSignature, source, config) = + static member FormatDocumentAsync(isSignature, source, ?config, ?cursor: Position) = let config = Option.defaultValue FormatConfig.Default config - - CodeFormatterImpl.getSourceText source - |> CodeFormatterImpl.formatDocument config isSignature + CodeFormatterImpl.formatDocument config isSignature (CodeFormatterImpl.getSourceText source) cursor static member FormatSelectionAsync(isSignature, source, selection, config) = let config = Option.defaultValue FormatConfig.Default config @@ -33,3 +31,5 @@ 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 diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index fe69ba9e92..c8b8a68aa0 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -5,21 +5,28 @@ open FSharp.Compiler.Syntax [] 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 a source string using an optional config - static member FormatDocumentAsync: isSignature: bool * source: string * ?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. + static member FormatASTAsync: ast: ParsedInput * ?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 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 @@ -27,3 +34,6 @@ 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 diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 46f3c2feb4..7d8712557e 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -51,22 +51,34 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin |> 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 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 @@ -74,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 @@ -85,7 +97,14 @@ let format (config: FormatConfig) (isSignature: bool) (source: ISourceText) : As | [] -> failwith "not possible" | [ (_, x) ] -> x | all -> - let allInFragments = all |> String.splitInFragments config.EndOfLine.NewLineString + // TODO: we currently ignore the cursor here. + // We would need to know which defines provided the code for each fragment. + // If we have a cursor, we need to find the fragment that contains it and matches the defines of the cursor. + + let allInFragments = + all + |> List.map (fun (dc, { Code = code }) -> dc, code) + |> String.splitInFragments config.EndOfLine.NewLineString let allHaveSameFragmentCount = let allWithCount = List.map (fun (_, f: string list) -> f.Length) allInFragments @@ -108,12 +127,12 @@ Please raise an issue at https://fsprojects.github.io/fantomas-tools/#/fantomas/ ) ) - List.map snd allInFragments - |> List.reduce String.merge - |> String.concat config.EndOfLine.NewLineString + let mergedCode = + List.map snd allInFragments + |> List.reduce String.merge + |> String.concat config.EndOfLine.NewLineString + + { Code = mergedCode; Cursor = None } 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 1e9e815146..40a57bf7a1 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fsi +++ b/src/Fantomas.Core/CodeFormatterImpl.fsi @@ -7,8 +7,10 @@ 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 dbf17ffaa2..570de72ce7 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -67,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 @@ -96,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 diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 1908074979..a3d1200483 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -1,6 +1,7 @@ module internal Fantomas.Core.Context open System +open FSharp.Compiler.Text open Fantomas.Core open Fantomas.Core.SyntaxOak @@ -178,16 +179,18 @@ module WriterEvents = | _ -> false) [] -type internal Context = +type Context = { Config: FormatConfig 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 WriterModel = WriterModel.init - WriterEvents = Queue.empty } + WriterEvents = Queue.empty + FormattedCursor = None } static member Create config : Context = { Context.Default with Config = config } @@ -266,16 +269,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 @@ -937,14 +944,14 @@ let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup 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 787b88bdba..b3e3002cbd 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -1,5 +1,6 @@ module internal Fantomas.Core.Context +open FSharp.Compiler.Text open Fantomas.Core.SyntaxOak type WriterEvent = @@ -51,10 +52,11 @@ type WriterModel = member IsDummy: bool [] -type internal Context = +type Context = { Config: FormatConfig WriterModel: WriterModel - WriterEvents: Queue } + WriterEvents: Queue + FormattedCursor: pos option } /// Initialize with a string writer and use space as delimiter static member Default: Context @@ -68,8 +70,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 @@ -259,7 +260,7 @@ val autoIndentAndNlnTypeUnlessStroustrup: f: (Type -> Context -> Context) -> t: 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/Fantomas.Core.fsproj b/src/Fantomas.Core/Fantomas.Core.fsproj index f7793f83b1..4dfd93c7c3 100644 --- a/src/Fantomas.Core/Fantomas.Core.fsproj +++ b/src/Fantomas.Core/Fantomas.Core.fsproj @@ -25,6 +25,7 @@ + diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index 842d36397b..27c79f6bce 100644 --- a/src/Fantomas.Core/Selection.fs +++ b/src/Fantomas.Core/Selection.fs @@ -407,11 +407,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/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index c9cbb770ae..db44c1be81 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -11,6 +11,7 @@ type TriviaContent = | BlockComment of string * newlineBefore: bool * newlineAfter: bool | Newline | Directive of string + | Cursor type TriviaNode(content: TriviaContent, range: range) = member val Content = content @@ -26,9 +27,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 +44,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 +56,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) @@ -1300,6 +1308,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 +1328,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 +1349,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 +1379,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 = diff --git a/src/Fantomas.Core/Trivia.fs b/src/Fantomas.Core/Trivia.fs index 5a2208a37f..dace6d4b8b 100644 --- a/src/Fantomas.Core/Trivia.fs +++ b/src/Fantomas.Core/Trivia.fs @@ -309,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 @@ -341,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 a1ec8c32f6..5ecb7272cb 100644 --- a/src/Fantomas.Core/Trivia.fsi +++ b/src/Fantomas.Core/Trivia.fsi @@ -6,3 +6,7 @@ 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.Tests/Integration/DaemonTests.fs b/src/Fantomas.Tests/Integration/DaemonTests.fs index 95544e8e88..70c64383c5 100644 --- a/src/Fantomas.Tests/Integration/DaemonTests.fs +++ b/src/Fantomas.Tests/Integration/DaemonTests.fs @@ -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/Daemon.fs b/src/Fantomas/Daemon.fs index 9aa45fefb6..f7793bd311 100644 --- a/src/Fantomas/Daemon.fs +++ b/src/Fantomas/Daemon.fs @@ -58,14 +58,27 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = parseOptionsFromEditorConfig config configProperties | None -> readConfiguration request.FilePath + let cursor = + request.Cursor + |> Option.map (fun cursor -> CodeFormatter.MakePosition(cursor.Line, cursor.Column)) + try - let! formatted = - CodeFormatter.FormatDocumentAsync(request.IsSignatureFile, request.SourceCode, config) + let! formatResponse = + CodeFormatter.FormatDocumentAsync( + request.IsSignatureFile, + request.SourceCode, + config, + ?cursor = cursor + ) - if formatted = request.SourceCode then + 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) } diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index e13b63de28..db4f2faf1e 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -48,7 +48,8 @@ let private formatContentInternalAsync try let isSignatureFile = Path.GetExtension(file) = ".fsi" - let! formattedContent = CodeFormatter.FormatDocumentAsync(isSignatureFile, originalContent, config) + let! { Code = formattedContent } = + CodeFormatter.FormatDocumentAsync(isSignatureFile, originalContent, config) let contentChanged = if compareWithoutLineEndings then From 05263a0c2cf5c2d6aa5aa4515361f8a782bb3081 Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Tue, 31 Jan 2023 01:22:32 -0700 Subject: [PATCH 06/34] Update output for copy-and-update expression for Stroustrup (#2748) * Implement stroustrup for copy-and-update expressions * Implement changes for anonymous records * Add some unit tests to demonstrate differences * Renamed files to match setting names. Expand on comparison in styles example. --------- Co-authored-by: nojaf --- ...s => AlignedMultilineBracketStyleTests.fs} | 91 ++++++++++++++++++- ...s => CrampedMultilineBracketStyleTests.fs} | 26 +----- .../Fantomas.Core.Tests.fsproj | 4 +- .../DotIndexedSetExpressionTests.fs | 18 ++-- .../Stroustrup/DotSetExpressionTests.fs | 6 +- .../KeepIndentInBranchExpressionTests.fs | 5 +- .../Stroustrup/LambdaExpressionTests.fs | 30 +++--- .../Stroustrup/LetOrUseBangExpressionTests.fs | 6 +- .../Stroustrup/LongIdentSetExpressionTests.fs | 6 +- ...LineLambdaClosingNewlineExpressionTests.fs | 28 +++--- .../NamedArgumentExpressionTests.fs | 12 +-- .../Stroustrup/SetExpressionTests.fs | 6 +- .../SynBindingFunctionExpressionTests.fs | 12 +-- ...ndingFunctionLongPatternExpressionTests.fs | 10 +- ...ngFunctionWithReturnTypeExpressionTests.fs | 12 +-- .../SynBindingValueExpressionTests.fs | 45 ++++++--- .../SynExprAndBangExpressionTests.fs | 6 +- .../SynMatchClauseExpressionTests.fs | 12 +-- .../YieldOrReturnBangExpressionTests.fs | 12 +-- .../YieldOrReturnExpressionTests.fs | 12 +-- src/Fantomas.Core/CodePrinter.fs | 52 +++++------ src/Fantomas.Core/SyntaxOak.fs | 9 +- 22 files changed, 249 insertions(+), 171 deletions(-) rename src/Fantomas.Core.Tests/{MultilineBlockBracketsOnSameColumnRecordTests.fs => AlignedMultilineBracketStyleTests.fs} (92%) rename src/Fantomas.Core.Tests/{RecordTests.fs => CrampedMultilineBracketStyleTests.fs} (98%) diff --git a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs similarity index 92% rename from src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs rename to src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs index b05ceaf2eb..235ef7f052 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnRecordTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnRecordTests +module Fantomas.Core.Tests.AlignedMultilineBracketStyleTests open NUnit.Framework open FsUnit @@ -1456,3 +1456,92 @@ 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 = ExperimentalStroustrup } + |> 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" ] +} +""" 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 583bb69c93..6b0076a5c1 100644 --- a/src/Fantomas.Core.Tests/RecordTests.fs +++ b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.Tests.RecordTests +module Fantomas.Core.Tests.CrampedMultilineBracketStyleTests open NUnit.Framework open FsUnit @@ -2124,30 +2124,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/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index 2c910cc6bb..ccb724615c 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -21,7 +21,7 @@ - + @@ -57,7 +57,7 @@ - + diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs index 66d33f056a..f9f69eaf8d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs @@ -45,10 +45,10 @@ myMutable.[x] <- |> should equal """ -myMutable.[x] <- - { astContext with +myMutable.[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -205,10 +205,10 @@ app().[x] <- |> should equal """ -app().[x] <- - { astContext with +app().[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -371,10 +371,10 @@ app(meh).[x] <- """ app( meh -).[x] <- - { astContext with +).[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs index 7e13318430..c9c021c6de 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs @@ -68,10 +68,10 @@ App().foo <- |> should equal """ -App().foo <- - { astContext with +App().foo <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs index e1bd30ccc1..540cf9a861 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs @@ -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 ab6914fd5b..f42cbad66e 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs @@ -45,10 +45,10 @@ fun x -> |> should equal """ -fun x -> - { astContext with +fun x -> { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -205,10 +205,10 @@ let ``paren lambda with update record`` () = |> should equal """ -(fun x -> - { astContext with +(fun x -> { + astContext with IsInsideMatchClausePattern = true - }) +}) """ [] @@ -365,10 +365,10 @@ List.map (fun x -> |> should equal """ -List.map (fun x -> - { astContext with +List.map (fun x -> { + astContext with IsInsideMatchClausePattern = true - }) +}) """ [] @@ -529,10 +529,10 @@ List.map (fun x -> equal """ List.map - (fun x -> - { astContext with + (fun x -> { + astContext with IsInsideMatchClausePattern = true - }) + }) b c """ @@ -713,13 +713,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() """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs index f095473fab..9bebd3a3cc 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs @@ -60,12 +60,12 @@ opt { equal """ opt { - let! foo = - { bar with + let! foo = { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } () } diff --git a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs index 208456d3a0..aa07affc2c 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs @@ -45,10 +45,10 @@ myMutable <- |> should equal """ -myMutable <- - { astContext with +myMutable <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs index 4705fe4e85..d94a51d744 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs @@ -46,11 +46,10 @@ let ``paren lambda with update record`` () = |> should equal """ -(fun x -> - { astContext with +(fun x -> { + astContext with IsInsideMatchClausePattern = true - } -) +}) """ [] @@ -207,11 +206,10 @@ List.map (fun x -> |> should equal """ -List.map (fun x -> - { astContext with +List.map (fun x -> { + astContext with IsInsideMatchClausePattern = true - } -) +}) """ [] @@ -372,11 +370,10 @@ List.map (fun x -> equal """ List.map - (fun x -> - { astContext with + (fun x -> { + astContext with IsInsideMatchClausePattern = true - } - ) + }) b c """ @@ -557,14 +554,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() """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs index 16a88577fa..bd7b190baf 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs @@ -59,13 +59,13 @@ let v = """ let v = SomeConstructor( - v = - { astContext with + v = { + astContext with IsInsideMatchClausePattern = true A = longTypeName B = someOtherVariable C = ziggyBarX - } + } ) """ @@ -313,13 +313,13 @@ let v = """ let v = new FooBar( - v = - { astContext with + v = { + astContext with IsInsideMatchClausePattern = true A = longTypeName B = someOtherVariable C = ziggyBarX - } + } ) """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs index 9aa5d0b4cb..5019f4c81c 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs @@ -45,10 +45,10 @@ myMutable[x] <- |> should equal """ -myMutable[x] <- - { astContext with +myMutable[x] <- { + astContext with IsInsideMatchClausePattern = true - } +} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs index 1208f15155..5d4a2d4b46 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs @@ -45,10 +45,10 @@ let x y = |> should equal """ -let x y = - { astContext with +let x y = { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -185,10 +185,10 @@ type Foo() = equal """ type Foo() = - member this.Bar x = - { astContext with + member this.Bar x = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs index 42565d873a..1db23e704e 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs @@ -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 d164bf3648..46fe7938ca 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs @@ -45,10 +45,10 @@ let x y : MyRecord = |> should equal """ -let x y : MyRecord = - { astContext with +let x y : MyRecord = { + astContext with IsInsideMatchClausePattern = true - } +} """ [] @@ -185,10 +185,10 @@ type Foo() = equal """ type Foo() = - member this.Bar x : MyRecord = - { astContext with + member this.Bar x : MyRecord = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs index e368e62ecb..d0c0e4c638 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs @@ -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" +|} """ [] @@ -235,10 +258,10 @@ type Foo() = equal """ type Foo() = - member this.Bar = - { astContext with + member this.Bar = { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -371,7 +394,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 +412,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 - |} +|} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs index bd5b52a0a9..19f80e7752 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs @@ -66,12 +66,12 @@ opt { opt { let! abc = def () - and! foo = - { bar with + and! foo = { + bar with X = xFieldValueOne Y = yFieldValueTwo Z = zFieldValueThree - } + } () } diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs index 6502c475c8..f16e94a968 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs @@ -49,10 +49,10 @@ match x with equal """ match x with -| _ -> - { astContext with +| _ -> { + astContext with IsInsideMatchClausePattern = true - } + } """ [] @@ -265,10 +265,10 @@ with ex -> """ try foo () -with ex -> - { astContext with +with ex -> { + astContext with IsInsideMatchClausePattern = true - } +} """ [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs index 47473ca144..3d61264029 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs @@ -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 - } + } } """ diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs index 8c2b46b72b..1d7841e7cb 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs @@ -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 - } + } } """ diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 570de72ce7..85cd9d1da3 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -501,15 +501,12 @@ let genExpr (e: Expr) = +> genSingleTextNode node.ClosingBrace | RecordNodeExtra.With we -> genSingleTextNode node.OpeningBrace - +> addSpaceIfSpaceAroundDelimiter - +> atCurrentColumnIndent (genExpr we) - +> !- " with" - +> indent - +> whenShortIndent indent - +> sepNln - +> fieldsExpr - +> unindent - +> whenShortIndent unindent + +> ifElseCtx + (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) + (indent +> sepNln) + addSpaceIfSpaceAroundDelimiter + +> genCopyExpr fieldsExpr we + +> onlyIfCtx (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) unindent +> sepNln +> genSingleTextNode node.ClosingBrace | RecordNodeExtra.None -> @@ -535,14 +532,7 @@ let genExpr (e: Expr) = | RecordNodeExtra.With we -> genSingleTextNode node.OpeningBrace +> addSpaceIfSpaceAroundDelimiter - +> atCurrentColumnIndent (genExpr we) - +> !- " with" - +> indent - +> whenShortIndent indent - +> sepNln - +> fieldsExpr - +> unindent - +> whenShortIndent unindent + +> genCopyExpr fieldsExpr we +> addSpaceIfSpaceAroundDelimiter +> genSingleTextNode node.ClosingBrace | RecordNodeExtra.None -> @@ -641,19 +631,13 @@ let genExpr (e: Expr) = 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 + +> ifElseCtx + (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) + (indent +> sepNln) + sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace + +> genCopyExpr fieldsExpr ci + +> onlyIfCtx (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) unindent +> sepNln +> genSingleTextNode node.ClosingBrace | None -> @@ -1735,6 +1719,16 @@ let genExpr (e: Expr) = | Expr.IndexFromEnd node -> !- "^" +> genExpr node.Expr |> genNode node | Expr.Typar node -> genSingleTextNode node +let genCopyExpr fieldsExpr ci = + atCurrentColumnIndent (genExpr ci) + +> !- " with" + +> indent + +> whenShortIndent indent + +> sepNln + +> fieldsExpr + +> whenShortIndent unindent + +> unindent + let genQuoteExpr (node: ExprQuoteNode) = genSingleTextNode node.OpenToken +> sepSpace diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index db44c1be81..7d85c01924 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -1732,13 +1732,10 @@ type Expr = match e with | Expr.Record node -> match node.Extra with - | RecordNodeExtra.Inherit _ - | RecordNodeExtra.With _ -> false + | RecordNodeExtra.Inherit _ -> false + | RecordNodeExtra.With _ | RecordNodeExtra.None -> true - | Expr.AnonRecord node -> - match node.CopyInfo with - | Some _ -> false - | None -> true + | Expr.AnonRecord _ -> true | Expr.NamedComputation node -> match node.Name with | Expr.Ident _ -> true From 3bc97ebb0d67fda283fbbcd5968e62aa4ca2a73a Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Wed, 1 Feb 2023 01:12:56 -0700 Subject: [PATCH 07/34] Refactor IsStroustrupStyleExpr and isStroustrupStyleType into Context.fs (#2756) --- src/Fantomas.Core/CodePrinter.fs | 18 +++------- src/Fantomas.Core/Context.fs | 62 +++++++++++++++++++++----------- src/Fantomas.Core/Context.fsi | 2 ++ src/Fantomas.Core/SyntaxOak.fs | 20 ----------- 4 files changed, 49 insertions(+), 53 deletions(-) diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 85cd9d1da3..6629ef5270 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -851,7 +851,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 @@ -1052,9 +1052,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 | _ -> @@ -1928,7 +1926,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 @@ -2187,9 +2185,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 @@ -2211,11 +2207,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 -> diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index a3d1200483..4886ef6694 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -752,21 +752,51 @@ let sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth expr (ctx: Context) expr ctx +let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = + let isStroustrupEnabled = config.MultilineBracketStyle = ExperimentalStroustrup + + match e with + | Expr.Record node when isStroustrupEnabled -> + match node.Extra with + | RecordNodeExtra.Inherit _ -> false + | RecordNodeExtra.With _ + | RecordNodeExtra.None -> true + | Expr.AnonRecord _ when isStroustrupEnabled -> true + | Expr.NamedComputation node when isStroustrupEnabled -> + match node.Name with + | Expr.Ident _ -> true + | _ -> false + | Expr.ArrayOrList _ when isStroustrupEnabled -> true + | _ -> false + +let isStroustrupStyleType (config: FormatConfig) (t: Type) = + let isStroustrupEnabled = config.MultilineBracketStyle = ExperimentalStroustrup + + 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 @@ -907,10 +937,7 @@ let addParenIfAutoNln expr f = let autoIndentAndNlnExpressUnlessStroustrup (f: Expr -> Context -> Context) (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 @@ -919,10 +946,7 @@ let autoIndentAndNlnExpressUnlessStroustrup (f: Expr -> Context -> Context) (e: let autoIndentAndNlnTypeUnlessStroustrup (f: Type -> Context -> Context) (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 @@ -935,9 +959,7 @@ let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup (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 diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index b3e3002cbd..0797c8daea 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -234,6 +234,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 diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index 7d85c01924..ec27b26d7e 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -382,11 +382,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) @@ -1728,21 +1723,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 _ -> false - | RecordNodeExtra.With _ - | RecordNodeExtra.None -> true - | Expr.AnonRecord _ -> 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 From 9b2e32c84ddbadc3fb81f00c25362d91297225f2 Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Wed, 1 Feb 2023 01:27:28 -0700 Subject: [PATCH 08/34] Drop `Experimental` prefix from `ExperimentalStroustrup`. (#2755) * Remove experimental prefix from stroustrup bracket style * Update docs * Remove experimental_stroustrup, Configuration.fsx tweaks. --------- Co-authored-by: nojaf --- docs/docs/end-users/Configuration.fsx | 94 ++++++++++--------- fantomas.sln | 1 + .../AlignedMultilineBracketStyleTests.fs | 2 +- src/Fantomas.Core.Tests/DallasTests.fs | 2 +- .../NumberOfItemsListOrArrayTests.fs | 8 +- .../DotIndexedSetExpressionTests.fs | 2 +- .../Stroustrup/DotSetExpressionTests.fs | 2 +- .../Stroustrup/ElmishTests.fs | 2 +- .../FunctionApplicationDualListTests.fs | 2 +- .../FunctionApplicationSingleListTests.fs | 2 +- .../KeepIndentInBranchExpressionTests.fs | 2 +- .../Stroustrup/LambdaExpressionTests.fs | 2 +- .../Stroustrup/LetOrUseBangExpressionTests.fs | 2 +- .../Stroustrup/LongIdentSetExpressionTests.fs | 2 +- ...LineLambdaClosingNewlineExpressionTests.fs | 2 +- .../NamedArgumentExpressionTests.fs | 2 +- .../Stroustrup/SetExpressionTests.fs | 2 +- .../SynBindingFunctionExpressionTests.fs | 2 +- ...ndingFunctionLongPatternExpressionTests.fs | 2 +- ...ngFunctionWithReturnTypeExpressionTests.fs | 2 +- .../SynBindingValueExpressionTests.fs | 2 +- .../SynExprAndBangExpressionTests.fs | 2 +- .../SynMatchClauseExpressionTests.fs | 2 +- .../SynTypeDefnSigReprSimpleTests.fs | 2 +- .../SynTypeDefnSimpleReprRecordTests.fs | 2 +- .../YieldOrReturnBangExpressionTests.fs | 2 +- .../YieldOrReturnExpressionTests.fs | 2 +- src/Fantomas.Core/CodePrinter.fs | 20 ++-- src/Fantomas.Core/Context.fs | 6 +- src/Fantomas.Core/FormatConfig.fs | 12 +-- .../EditorConfigurationTests.fs | 6 +- 31 files changed, 101 insertions(+), 94 deletions(-) diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index b7a2c394a1..72501fc13e 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,13 +45,13 @@ You can quickly try your settings via the *) -#r "nuget: Fantomas.Core, 5.2.0-alpha-012" - -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 (** @@ -63,7 +69,7 @@ However, there are settings that we do not recommend and generally should not be -` indent_size` has to be between 1 and 10. +`indent_size` has to be between 1 and 10. This preference sets the indentation The common values are 2 and 4. @@ -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,14 +575,14 @@ formatCode """ { FormatConfig.Default with MaxDotGetExpressionWidth = 100 } -(*** include-it ***) +(*** include-output ***) (** `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. -`ExperimentalStroustrup` 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). +`Stroustrup` Allow for easier reordering of members and keeping the code succinct. Default = Cramped. *) @@ -606,7 +612,7 @@ formatCode """ { FormatConfig.Default with MultilineBracketStyle = Aligned } -(*** include-it ***) +(*** include-output ***) formatCode """ @@ -632,8 +638,8 @@ formatCode (19, 20, 21) |] """ { FormatConfig.Default with - MultilineBracketStyle = ExperimentalStroustrup } -(*** include-it ***) + MultilineBracketStyle = Stroustrup } +(*** include-output ***) (** ## G-Research style @@ -657,7 +663,7 @@ type Range = """ { FormatConfig.Default with NewlineBetweenTypeDefinitionAndMembers = true } -(*** include-it ***) +(*** include-output ***) (** @@ -679,7 +685,7 @@ let run """ { FormatConfig.Default with AlignFunctionSignatureToIndentation = true } -(*** include-it ***) +(*** include-output ***) (** @@ -712,7 +718,7 @@ type D() = """ { FormatConfig.Default with AlternativeLongMemberDefinitions = true } -(*** include-it ***) +(*** include-output ***) (** @@ -740,7 +746,7 @@ let printListWithOffset a list1 = """ { FormatConfig.Default with MultiLineLambdaClosingNewline = true } -(*** include-it ***) +(*** include-output ***) (** @@ -770,7 +776,7 @@ let main argv = """ { FormatConfig.Default with ExperimentalKeepIndentInBranch = true } -(*** include-it ***) +(*** include-output ***) (** @@ -788,7 +794,7 @@ formatCode { FormatConfig.Default with BarBeforeDiscriminatedUnionDeclaration = true } -(*** include-it ***) +(*** include-output ***) (** ## Other @@ -822,7 +828,7 @@ formatCode """ { FormatConfig.Default with BlankLinesAroundNestedMultilineExpressions = false } -(*** include-it ***) +(*** include-output ***) (** @@ -840,7 +846,7 @@ formatCode """ { FormatConfig.Default with KeepMaxNumberOfBlankLines = 1 } -(*** include-it ***) +(*** include-output ***) (** @@ -867,7 +873,7 @@ formatCode """ { FormatConfig.Default with StrictMode = true } -(*** include-it ***) +(*** include-output ***) (** diff --git a/fantomas.sln b/fantomas.sln index ba8edd6db9..f0e27002b7 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}" diff --git a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs index 235ef7f052..9389d84b13 100644 --- a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs @@ -1526,7 +1526,7 @@ let ``update record in stroustrup style`` () = let v = { rainbow with Boss = "Jeffrey" ; Lackeys = [ "Zippy"; "George"; "Bungle" ] } """ { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/DallasTests.fs b/src/Fantomas.Core.Tests/DallasTests.fs index 039a54707c..2ecbbc8ddd 100644 --- a/src/Fantomas.Core.Tests/DallasTests.fs +++ b/src/Fantomas.Core.Tests/DallasTests.fs @@ -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/NumberOfItemsListOrArrayTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs index 44a00d904e..6d4caa649a 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs @@ -88,7 +88,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal @@ -206,7 +206,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal @@ -240,7 +240,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal @@ -272,7 +272,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs index f9f69eaf8d..ea829c6124 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs index c9c021c6de..b95a5b7d45 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs b/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs index 9272a1064e..692a4c36cd 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``long named arguments should go on newline`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs index b2fc44e629..e45b9cd5f7 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``two short lists`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs index 4da71aaef8..5a7d4b6111 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``short function application`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs index 540cf9a861..55054b5833 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/KeepIndentInBranchExpressionTests.fs @@ -10,7 +10,7 @@ open Fantomas.Core 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. diff --git a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs index f42cbad66e..887c056d32 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs index 9bebd3a3cc..9c1376e62d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs index aa07affc2c..b8f4f287e9 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs index d94a51d744..26a92441c8 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs @@ -8,7 +8,7 @@ open Fantomas.Core let config = { config with MultiLineLambdaClosingNewline = true - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs index bd7b190baf..2e6a07ead7 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs index 5019f4c81c..d175588d89 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs index 5d4a2d4b46..4bef1fbbe4 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs index 1db23e704e..3c990d3c51 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionLongPatternExpressionTests.fs @@ -8,7 +8,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with MaxLineLength = 80 - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } // TODO: conclude on what should happen here diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs index 46fe7938ca..721d834f4c 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs index d0c0e4c638..0f6eef956c 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs index 19f80e7752..d74b31ded0 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs index f16e94a968..aa8b9ca295 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs index 2f4698f006..3fdb9c16eb 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``record type definition`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs index 8c86841863..fa6cbce5aa 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = ExperimentalStroustrup } + MultilineBracketStyle = Stroustrup } [] let ``record type definition`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs index 3d61264029..80e58f97fc 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs index 1d7841e7cb..7a859b8610 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core.Tests.TestHelper let config = { config with - MultilineBracketStyle = ExperimentalStroustrup + MultilineBracketStyle = Stroustrup MaxArrayOrListWidth = 40 } [] diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 6629ef5270..874bce400b 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -502,11 +502,11 @@ let genExpr (e: Expr) = | RecordNodeExtra.With we -> genSingleTextNode node.OpeningBrace +> ifElseCtx - (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) + (fun ctx -> ctx.Config.IsStroustrupStyle) (indent +> sepNln) addSpaceIfSpaceAroundDelimiter +> genCopyExpr fieldsExpr we - +> onlyIfCtx (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) unindent + +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent +> sepNln +> genSingleTextNode node.ClosingBrace | RecordNodeExtra.None -> @@ -633,11 +633,11 @@ let genExpr (e: Expr) = | Some ci -> genSingleTextNodeSuffixDelimiter node.OpeningBrace +> ifElseCtx - (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) + (fun ctx -> ctx.Config.IsStroustrupStyle) (indent +> sepNln) sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace +> genCopyExpr fieldsExpr ci - +> onlyIfCtx (fun ctx -> ctx.Config.ExperimentalStroustrupStyle) unindent + +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent +> sepNln +> genSingleTextNode node.ClosingBrace | None -> @@ -2128,7 +2128,7 @@ let genFunctionNameWithMultilineLids (trailing: Context -> Context) (longIdent: |> genNode parentNode let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.ExperimentalStroustrupStyle then + if not config.IsStroustrupStyle then None else let mutable otherArgs = ListCollector() @@ -2145,7 +2145,7 @@ let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = visit appNode.Arguments let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.ExperimentalStroustrupStyle then + if not config.IsStroustrupStyle then None else let mutable otherArgs = ListCollector() @@ -3232,9 +3232,9 @@ let genTypeDefn (td: TypeDefn) = +> genMemberDefnList members match ctx.Config.MultilineBracketStyle with - | ExperimentalStroustrup when hasNoMembers -> stroustrupWithoutMembers ctx + | Stroustrup when hasNoMembers -> stroustrupWithoutMembers ctx | Aligned - | ExperimentalStroustrup -> aligned ctx + | Stroustrup -> aligned ctx | Cramped when anyFieldHasXmlDoc -> aligned ctx | Cramped -> cramped ctx @@ -3258,7 +3258,7 @@ let genTypeDefn (td: TypeDefn) = let size = getRecordSize ctx node.Fields let short = bodyExpr size - if ctx.Config.ExperimentalStroustrupStyle && hasNoMembers then + if ctx.Config.IsStroustrupStyle && hasNoMembers then (sepSpace +> short) ctx else isSmallExpression size short (indentSepNlnUnindent short) ctx @@ -3270,7 +3270,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 diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 4886ef6694..dd65d6dca3 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -753,7 +753,7 @@ let sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth expr (ctx: Context) ctx let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = - let isStroustrupEnabled = config.MultilineBracketStyle = ExperimentalStroustrup + let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup match e with | Expr.Record node when isStroustrupEnabled -> @@ -770,7 +770,7 @@ let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = | _ -> false let isStroustrupStyleType (config: FormatConfig) (t: Type) = - let isStroustrupEnabled = config.MultilineBracketStyle = ExperimentalStroustrup + let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup match t with | Type.AnonRecord _ when isStroustrupEnabled -> true @@ -896,7 +896,7 @@ let ifAlignOrStroustrupBrackets f g = (fun ctx -> match ctx.Config.MultilineBracketStyle with | Aligned - | ExperimentalStroustrup -> true + | Stroustrup -> true | Cramped -> false) f g diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index 115e0b4dea..b62da15643 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -26,19 +26,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 +210,8 @@ type FormatConfig = BarBeforeDiscriminatedUnionDeclaration: bool [] - [] - [] + [] + [] MultilineBracketStyle: MultilineBracketStyle [] @@ -223,7 +223,7 @@ type FormatConfig = [] StrictMode: bool } - member x.ExperimentalStroustrupStyle = x.MultilineBracketStyle = ExperimentalStroustrup + member x.IsStroustrupStyle = x.MultilineBracketStyle = Stroustrup static member Default = { IndentSize = 4 diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 45158793b0..7eadffe4cb 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -448,13 +448,13 @@ insert_final_newline = false Assert.IsFalse config.InsertFinalNewline [] -let ``fsharp_multiline_bracket_style = experimental_stroustrup`` () = +let ``fsharp_multiline_bracket_style = stroustrup`` () = let rootDir = tempName () let editorConfig = """ [*.fs] -fsharp_multiline_bracket_style = experimental_stroustrup +fsharp_multiline_bracket_style = stroustrup """ use configFixture = @@ -464,7 +464,7 @@ fsharp_multiline_bracket_style = experimental_stroustrup let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.AreEqual(ExperimentalStroustrup, config.MultilineBracketStyle) + Assert.AreEqual(Stroustrup, config.MultilineBracketStyle) [] let ``fsharp_multiline_bracket_style = aligned`` () = From 43c5e0b14d9ddc170f73f443a0de8bc2f7a145d7 Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Wed, 1 Feb 2023 16:39:08 +0000 Subject: [PATCH 09/34] Expose SyntaxOak API (#2758) * Expose SyntaxOak API * Add changelog entry for 6.0 alpha 2. --------- Co-authored-by: nojaf --- CHANGELOG.md | 7 +++++++ src/Fantomas.Core/CodeFormatter.fs | 28 ++++++++++++++++++++++++++++ src/Fantomas.Core/CodeFormatter.fsi | 10 ++++++++++ src/Fantomas.Core/SyntaxOak.fs | 2 +- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd31257ff..902d73c7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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 diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 99d2d28cb6..f8e7b2d352 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -2,6 +2,7 @@ namespace Fantomas.Core open FSharp.Compiler.Syntax open FSharp.Compiler.Text +open Fantomas.Core.SyntaxOak [] type CodeFormatter = @@ -33,3 +34,30 @@ type CodeFormatter = 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) + } + + static member FormatOakAsync(oak: Oak) : Async = + async { + let context = Context.Context.Create 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 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 c8b8a68aa0..c1b5a8ba9f 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -2,6 +2,7 @@ namespace Fantomas.Core open FSharp.Compiler.Text open FSharp.Compiler.Syntax +open Fantomas.Core.SyntaxOak [] type CodeFormatter = @@ -37,3 +38,12 @@ type CodeFormatter = /// 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/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index ec27b26d7e..2d01663f39 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -1,4 +1,4 @@ -module internal rec Fantomas.Core.SyntaxOak +module rec Fantomas.Core.SyntaxOak open System.Collections.Generic open FSharp.Compiler.Text From ac3b8ac34fa2925e752a8269f8726a004ae42ed4 Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Thu, 2 Feb 2023 00:53:44 -0700 Subject: [PATCH 10/34] Extract setting for preference of newline placement for computation expressions (#2746) * Add new setting * Add test * Implement new setting * Add setting-specific tests to its own file * Fix an issue with applying to lambdas * Fix issue when applying to LetBang and AndBang * Fix all failing tests, some cleanup * Remove invalid tests, some more cleanup * Rename some helpers * temp * Cleanup, add test * Rename setting * Use local Fantomas.Core reference for Configuration.fsx. * Rename some things and fix assertion * Update docs * Simplify isStroustrupStyleExpr * Rename test file to match setting name. * Add additional test to highlight that settings don't affect each other. --------- Co-authored-by: nojaf --- docs/docs/end-users/Configuration.fsx | 20 + .../Fantomas.Core.Tests.fsproj | 1 + ...foreMultilineComputationExpressionTests.fs | 724 ++++++++++++++++++ .../DotIndexedSetExpressionTests.fs | 68 -- .../Stroustrup/DotSetExpressionTests.fs | 22 - .../Stroustrup/LambdaExpressionTests.fs | 116 --- .../Stroustrup/LetOrUseBangExpressionTests.fs | 29 - .../Stroustrup/LongIdentSetExpressionTests.fs | 22 - ...LineLambdaClosingNewlineExpressionTests.fs | 94 --- .../NamedArgumentExpressionTests.fs | 56 -- .../Stroustrup/SetExpressionTests.fs | 22 - .../SynBindingFunctionExpressionTests.fs | 46 -- ...ngFunctionWithReturnTypeExpressionTests.fs | 46 -- .../SynBindingValueExpressionTests.fs | 46 -- .../SynExprAndBangExpressionTests.fs | 32 - .../SynMatchClauseExpressionTests.fs | 51 -- .../YieldOrReturnBangExpressionTests.fs | 36 - .../YieldOrReturnExpressionTests.fs | 36 - src/Fantomas.Core/CodePrinter.fs | 10 +- src/Fantomas.Core/Context.fs | 23 +- src/Fantomas.Core/Context.fsi | 4 +- src/Fantomas.Core/FormatConfig.fs | 5 + .../EditorConfigurationTests.fs | 19 + 23 files changed, 786 insertions(+), 742 deletions(-) create mode 100644 src/Fantomas.Core.Tests/NewlineBeforeMultilineComputationExpressionTests.fs diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index 72501fc13e..a205f1f6f1 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -641,6 +641,26 @@ formatCode MultilineBracketStyle = Stroustrup } (*** include-output ***) +(** + + +Insert a newline before a computation expression that spans multiple lines + +Default = true +*) + +formatCode + """ + let something = + task { + let! thing = otherThing () + return 5 + } + """ + { FormatConfig.Default with + NewlineBeforeMultilineComputationExpression = false } +(*** include-output ***) + (** ## G-Research style diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index ccb724615c..ec59e55cdf 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -35,6 +35,7 @@ + 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/Stroustrup/DotIndexedSetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs index ea829c6124..8f881af18e 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotIndexedSetExpressionTests.fs @@ -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 @@ -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 @@ -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 b95a5b7d45..db191b34db 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/DotSetExpressionTests.fs @@ -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/LambdaExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs index 887c056d32..d1c80fd6b3 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LambdaExpressionTests.fs @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 9c1376e62d..6c2eee6a1b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LetOrUseBangExpressionTests.fs @@ -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 b8f4f287e9..f4b7386886 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/LongIdentSetExpressionTests.fs @@ -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 26a92441c8..6cd4f24c8d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/MultiLineLambdaClosingNewlineExpressionTests.fs @@ -97,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 @@ -257,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 @@ -429,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 @@ -614,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 2e6a07ead7..e071d88ba6 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/NamedArgumentExpressionTests.fs @@ -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 @@ -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/SetExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs index d175588d89..bfc83edf8d 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SetExpressionTests.fs @@ -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 4bef1fbbe4..219ec5e8d6 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionExpressionTests.fs @@ -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 @@ -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/SynBindingFunctionWithReturnTypeExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs index 721d834f4c..19ff09f198 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingFunctionWithReturnTypeExpressionTests.fs @@ -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 @@ -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 0f6eef956c..654c191c57 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs @@ -119,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 @@ -313,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 diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs index d74b31ded0..c3d0e79b21 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynExprAndBangExpressionTests.fs @@ -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/SynMatchClauseExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs index aa8b9ca295..959701005b 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynMatchClauseExpressionTests.fs @@ -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 @@ -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/YieldOrReturnBangExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs index 80e58f97fc..9c0c9f6cdc 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnBangExpressionTests.fs @@ -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 7a859b8610..114d7151ae 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/YieldOrReturnExpressionTests.fs @@ -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/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 874bce400b..850b282116 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -1376,7 +1376,7 @@ let genExpr (e: Expr) = clauseNode.WhenExpr +> sepSpace +> genSingleTextNodeWithSpaceSuffix sepSpace clauseNode.Arrow - +> autoIndentAndNlnExpressUnlessStroustrup genExpr clauseNode.BodyExpr + +> indentSepNlnUnindentExprUnlessStroustrup genExpr clauseNode.BodyExpr +> leaveNode clauseNode atCurrentColumn ( @@ -1828,7 +1828,7 @@ let genNamedArgumentExpr (node: ExprInfixAppNode) = genExpr node.LeftHandSide +> sepSpace +> genSingleTextNode node.Operator - +> autoIndentAndNlnExpressUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide + +> indentSepNlnUnindentExprUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide expressionFitsOnRestOfLine short long |> genNode node @@ -2689,7 +2689,7 @@ let genBinding (b: BindingNode) (ctx: Context) : Context = let short = sepSpace +> body let long = - autoIndentAndNlnExpressUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr + indentSepNlnUnindentExprUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr isShortExpression ctx.Config.MaxFunctionBindingWidth short long @@ -2778,7 +2778,9 @@ 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 +> indentSepNlnUnindentExprUnlessStroustrup genExpr b.Expr + isShortExpression ctx.Config.MaxValueBindingWidth short long ctx) genNode b binding ctx diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index dd65d6dca3..0b88cad3dd 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -756,17 +756,14 @@ let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup match e with - | Expr.Record node when isStroustrupEnabled -> + | Expr.Record node -> match node.Extra with | RecordNodeExtra.Inherit _ -> false | RecordNodeExtra.With _ - | RecordNodeExtra.None -> true - | Expr.AnonRecord _ when isStroustrupEnabled -> true - | Expr.NamedComputation node when isStroustrupEnabled -> - match node.Name with - | Expr.Ident _ -> true - | _ -> false - | Expr.ArrayOrList _ when isStroustrupEnabled -> true + | RecordNodeExtra.None -> isStroustrupEnabled + | Expr.AnonRecord _ + | Expr.ArrayOrList _ -> isStroustrupEnabled + | Expr.NamedComputation _ -> not config.NewlineBeforeMultilineComputationExpression | _ -> false let isStroustrupStyleType (config: FormatConfig) (t: Type) = @@ -935,7 +932,7 @@ 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 indentSepNlnUnindentExprUnlessStroustrup f (e: Expr) (ctx: Context) = let shouldUseStroustrup = isStroustrupStyleExpr ctx.Config e && canSafelyUseStroustrup (Expr.Node e) @@ -944,7 +941,7 @@ let autoIndentAndNlnExpressUnlessStroustrup (f: Expr -> Context -> Context) (e: else indentSepNlnUnindent (f e) ctx -let autoIndentAndNlnTypeUnlessStroustrup (f: Type -> Context -> Context) (t: Type) (ctx: Context) = +let autoIndentAndNlnTypeUnlessStroustrup f (t: Type) (ctx: Context) = let shouldUseStroustrup = isStroustrupStyleType ctx.Config t && canSafelyUseStroustrup (Type.Node t) @@ -953,11 +950,7 @@ let autoIndentAndNlnTypeUnlessStroustrup (f: Type -> Context -> Context) (t: Typ else autoIndentAndNlnIfExpressionExceedsPageWidth (f t) ctx -let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup - (f: Expr -> Context -> Context) - (e: Expr) - (ctx: Context) - = +let autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup f (e: Expr) (ctx: Context) = let isStroustrup = isStroustrupStyleExpr ctx.Config e && canSafelyUseStroustrup (Expr.Node e) diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index 0797c8daea..7bb4b8db3a 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -256,7 +256,9 @@ 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 indentSepNlnUnindentExprUnlessStroustrup: f: (Expr -> Context -> Context) -> e: Expr -> ctx: Context -> Context + val autoIndentAndNlnTypeUnlessStroustrup: f: (Type -> Context -> Context) -> t: Type -> ctx: Context -> Context val autoIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup: diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index b62da15643..6dd33c14ac 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -218,6 +218,10 @@ type FormatConfig = [] KeepMaxNumberOfBlankLines: Num + [] + [] + NewlineBeforeMultilineComputationExpression: bool + [] [] [] @@ -261,4 +265,5 @@ type FormatConfig = BarBeforeDiscriminatedUnionDeclaration = false MultilineBracketStyle = Cramped KeepMaxNumberOfBlankLines = 100 + NewlineBeforeMultilineComputationExpression = true StrictMode = false } diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 7eadffe4cb..1cde1e3b9a 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -503,3 +503,22 @@ fsharp_multiline_bracket_style = cramped let config = EditorConfig.readConfiguration fsharpFile.FSharpFile Assert.AreEqual(Cramped, config.MultilineBracketStyle) + +[] +let fsharp_prefer_computation_expression_name_on_same_line () = + let rootDir = tempName () + + let editorConfig = + """ +[*.fs] +fsharp_newline_before_multiline_computation_expression = false +""" + + use configFixture = + new ConfigurationFile(defaultConfig, rootDir, content = editorConfig) + + use fsharpFile = new FSharpFile(rootDir) + + let config = EditorConfig.readConfiguration fsharpFile.FSharpFile + + Assert.IsFalse config.NewlineBeforeMultilineComputationExpression From c83a7d122b2fc4aacb974e0678f1d29ffef5362f Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 3 Feb 2023 08:49:55 +0100 Subject: [PATCH 11/34] Remove F# option from public API. (#2759) * Remove F# option from public API. * Apply suggestions from code review Co-authored-by: dawe --------- Co-authored-by: dawe --- src/Fantomas.Core/CodeFormatter.fs | 23 +++++++++++++++++------ src/Fantomas.Core/CodeFormatter.fsi | 19 ++++++++++++++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index f8e7b2d352..9efa706e69 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -9,19 +9,31 @@ type CodeFormatter = static member ParseAsync(isSignature, source) : Async<(ParsedInput * string list) array> = CodeFormatterImpl.getSourceText source |> CodeFormatterImpl.parse isSignature - static member FormatASTAsync(ast: ParsedInput, ?source, ?config) : Async = - let sourceText = Option.map CodeFormatterImpl.getSourceText source - let config = Option.defaultValue FormatConfig.Default config + static member FormatASTAsync(ast: ParsedInput) : Async = + CodeFormatterImpl.formatAST ast None FormatConfig.Default None |> async.Return + + static member FormatASTAsync(ast: ParsedInput, config) : Async = + CodeFormatterImpl.formatAST ast None config None |> async.Return + + static member FormatASTAsync(ast: ParsedInput, source) : Async = + let sourceText = Some(CodeFormatterImpl.getSourceText source) + + CodeFormatterImpl.formatAST ast sourceText FormatConfig.Default None + |> async.Return + static member FormatASTAsync(ast: ParsedInput, source, config) : Async = + let sourceText = Some(CodeFormatterImpl.getSourceText source) CodeFormatterImpl.formatAST ast sourceText config None |> async.Return static member FormatDocumentAsync(isSignature, source, ?config, ?cursor: Position) = let config = Option.defaultValue FormatConfig.Default config CodeFormatterImpl.formatDocument config isSignature (CodeFormatterImpl.getSourceText source) cursor - static member FormatSelectionAsync(isSignature, source, selection, config) = - let config = Option.defaultValue FormatConfig.Default config + static member FormatSelectionAsync(isSignature, source, selection) = + CodeFormatterImpl.getSourceText source + |> Selection.formatSelection FormatConfig.Default isSignature selection + static member FormatSelectionAsync(isSignature, source, selection, config) = CodeFormatterImpl.getSourceText source |> Selection.formatSelection config isSignature selection @@ -35,7 +47,6 @@ type CodeFormatter = 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 diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index c1b5a8ba9f..e64a873959 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -9,8 +9,17 @@ type CodeFormatter = /// 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 an abstract syntax tree using a given config + static member FormatASTAsync: ast: ParsedInput * config: FormatConfig -> Async + + /// Format an abstract syntax tree with the original source for trivia processing + static member FormatASTAsync: ast: ParsedInput * source: string -> Async + + /// Format an abstract syntax tree with the original source for trivia processing using a given config + static member FormatASTAsync: ast: ParsedInput * source: string * config: FormatConfig -> Async /// /// Format a source string using an optional config. @@ -22,10 +31,14 @@ type CodeFormatter = 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. static member IsValidFSharpCodeAsync: isSignature: bool * source: string -> Async From 7bcd5a5a8d4a57dde69ffc6f68ee89f86b83b9f1 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sat, 4 Feb 2023 15:41:46 +0100 Subject: [PATCH 12/34] Adds naive parallel formatting implementation. --- src/Fantomas.Core.Tests/TypeProviderTests.fs | 3 +- src/Fantomas.Core/CodeFormatterImpl.fs | 4 +- src/Fantomas.Core/FormatConfig.fs | 3 + src/Fantomas.Tests/Integration/ConfigTests.fs | 2 +- .../Integration/IgnoreFilesTests.fs | 4 +- src/Fantomas/Fantomas.fsproj | 2 + src/Fantomas/Format.fs | 3 +- src/Fantomas/Logging.fs | 23 +- src/Fantomas/Logging.fsi | 19 ++ src/Fantomas/Program.fs | 272 +++++++++++------- 10 files changed, 220 insertions(+), 115 deletions(-) create mode 100644 src/Fantomas/Logging.fsi diff --git a/src/Fantomas.Core.Tests/TypeProviderTests.fs b/src/Fantomas.Core.Tests/TypeProviderTests.fs index 7735df89b4..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/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 7d8712557e..1ddaf29829 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -26,7 +26,7 @@ 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, []) |] } @@ -44,7 +44,7 @@ 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{diagnostics}") + raise (ParseException diagnostics) return (untypedTree, defineCombination) }) diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index 6dd33c14ac..c1c329d811 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -2,6 +2,9 @@ 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) diff --git a/src/Fantomas.Tests/Integration/ConfigTests.fs b/src/Fantomas.Tests/Integration/ConfigTests.fs index 72c6e1544c..ddaa753bc9 100644 --- a/src/Fantomas.Tests/Integration/ConfigTests.fs +++ b/src/Fantomas.Tests/Integration/ConfigTests.fs @@ -52,7 +52,7 @@ end_of_line=cr """ ) - let args = sprintf "%s %s" NormalVerbosity 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) diff --git a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs index 5c5a1fbd6f..787cea8b10 100644 --- a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs +++ b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs @@ -88,7 +88,7 @@ let ``ignore file in folder`` () = exitCode |> should equal 0 File.ReadAllText inputFixture.Filename |> should equal Source - output |> should contain "Processed files: 0" + output |> should contain "Formatted: 0, Ignored : 0, Unchanged : 0, Errored: 0" [] let ``ignore file while checking`` () = @@ -135,4 +135,4 @@ let ``honor ignore file when processing a folder`` () = runFantomasTool (sprintf "%s .%c%s" Verbosity Path.DirectorySeparatorChar subFolder) output |> should not' (contain "ignored") - output |> should contain "Processed files: 1" + output |> should contain "Formatted: 1, Ignored : 0, Unchanged : 0, Errored: 0" diff --git a/src/Fantomas/Fantomas.fsproj b/src/Fantomas/Fantomas.fsproj index 638b316507..65464c0356 100644 --- a/src/Fantomas/Fantomas.fsproj +++ b/src/Fantomas/Fantomas.fsproj @@ -12,6 +12,7 @@ true Fantomas false + --test:ParallelCheckingWithSignatureFilesOn @@ -20,6 +21,7 @@ + diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index db4f2faf1e..f6e983f2b7 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -81,9 +81,10 @@ let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: str if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then async { return IgnoredFile file } else - let originalContent = File.ReadAllText file async { + let! originalContent = File.ReadAllTextAsync file |> Async.AwaitTask + let! formatted = originalContent |> formatContentInternalAsync compareWithoutLineEndings config file diff --git a/src/Fantomas/Logging.fs b/src/Fantomas/Logging.fs index 5ff6d9ea11..c6ebda69a0 100644 --- a/src/Fantomas/Logging.fs +++ b/src/Fantomas/Logging.fs @@ -7,9 +7,20 @@ type VerbosityLevel = | Normal | Detailed -let private logger = - Log.Logger <- LoggerConfiguration().WriteTo.Console().CreateLogger() - Log.Logger +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) @@ -18,10 +29,6 @@ let stdlog (s: string) = logger.Information(s) let elog (s: string) = logger.Error(s) /// log a message if the verbosity level is >= Detailed -let logGrEqDetailed verbosity s = - if verbosity = VerbosityLevel.Detailed then - logger.Information(s) - else - () +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 83d950d1da..d896a645a8 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -6,7 +6,6 @@ open Fantomas.Daemon open Fantomas.Logging open Argu open System.Text -open Fantomas.Format let extensions = set [| ".fs"; ".fsx"; ".fsi"; ".ml"; ".mli" |] @@ -17,8 +16,8 @@ type Arguments = | [] Out of string | [] Check | [] Daemon - | [] Version - | [] Verbosity of string + | [] Version + | [] Verbosity of string | [] Input of string list interface IArgParserTemplate with @@ -39,12 +38,14 @@ type Arguments = (Seq.map (fun s -> "*" + s) extensions |> String.concat ",") | Verbosity _ -> "Set the verbosity level. Allowed values are n[ormal] and d[etailed]." -let time f = - let sw = Diagnostics.Stopwatch.StartNew() - let res = f () - sw.Stop() - stdlog $"Time taken: %O{sw.Elapsed} s" - res +let timeAsync f = + async { + let sw = Diagnostics.Stopwatch.StartNew() + let! res = f () + sw.Stop() + stdlog $"Time taken: %O{sw.Elapsed} s" + return res + } [] type InputPath = @@ -60,6 +61,13 @@ type OutputPath = | IO of string | NotKnown +[] +type ProcessResult = + | Formatted of string + | Ignored of string + | Unchanged of string + | Error of string * exn + let isInExcludedDir (fullPath: string) = set [| "obj"; ".fable"; "fable_modules"; "node_modules" |] |> Set.map (fun dir -> sprintf "%c%s%c" Path.DirectorySeparatorChar dir Path.DirectorySeparatorChar) @@ -85,58 +93,81 @@ let rec allFiles isRec path = /// 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 + } /// Format a source string using given config and write to a text writer -let processSourceString verbosity (force: bool) s (fileName: string) config = +let processSourceString (force: 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 + + if hasBom then + do! File.WriteAllTextAsync(fileName, formatted, Encoding.UTF8) |> Async.AwaitTask + else + do! File.WriteAllTextAsync(fileName, formatted) |> Async.AwaitTask - logGrEqDetailed verbosity $"%s{fileName} has been written." + logGrEqDetailed $"%s{fileName} has been written." + } async { let! formatted = s |> Format.formatContentAsync config fileName match formatted with - | Format.FormatResult.Formatted(_, formattedContent) -> formattedContent |> writeResult + | Format.FormatResult.Formatted(_, formattedContent) -> + do! formattedContent |> writeResult + return ProcessResult.Formatted(fileName) | Format.InvalidCode(file, formattedContent) when force -> stdlog $"%s{file} was not valid after formatting." - formattedContent |> writeResult - | Format.FormatResult.Unchanged file -> logGrEqDetailed verbosity $"'%s{file}' was unchanged" - | Format.IgnoredFile file -> logGrEqDetailed verbosity $"'%s{file}' was ignored" - | Format.FormatResult.Error(_, ex) -> raise ex - | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") + do! formattedContent |> writeResult + return ProcessResult.Formatted(fileName) + | Format.FormatResult.Unchanged file -> + logGrEqDetailed $"'%s{file}' was unchanged" + return ProcessResult.Unchanged(fileName) + | Format.IgnoredFile file -> + logGrEqDetailed $"'%s{file}' was ignored" + return ProcessResult.Ignored fileName + | Format.FormatResult.Error(file, ex) -> return ProcessResult.Error(file, ex) + | Format.InvalidCode(file, _) -> + let ex = FormatException($"Formatting {file} lead to invalid F# code") + return ProcessResult.Error(file, ex) } - |> Async.RunSynchronously /// Format inFile and write to text writer -let processSourceFile verbosity (force: bool) inFile (tw: TextWriter) = +let processSourceFile (force: bool) inFile (tw: TextWriter) = async { let! formatted = Format.formatFileAsync inFile match formatted with - | Format.FormatResult.Formatted(_, formattedContent) -> tw.Write(formattedContent) + | Format.FormatResult.Formatted(_, formattedContent) -> + do! tw.WriteAsync(formattedContent) |> Async.AwaitTask + return ProcessResult.Formatted(inFile) | Format.InvalidCode(file, formattedContent) when force -> stdlog $"%s{file} was not valid after formatting." - tw.Write(formattedContent) - | Format.FormatResult.Unchanged _ -> inFile |> File.ReadAllText |> tw.Write - | Format.IgnoredFile file -> logGrEqDetailed verbosity $"'%s{file}' was ignored" - | Format.FormatResult.Error(_, ex) -> raise ex - | Format.InvalidCode(file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") + do! tw.WriteAsync(formattedContent) |> Async.AwaitTask + return ProcessResult.Formatted(inFile) + | Format.FormatResult.Unchanged _ -> + let! input = inFile |> File.ReadAllTextAsync |> Async.AwaitTask + do! input |> tw.WriteAsync |> Async.AwaitTask + return ProcessResult.Unchanged inFile + | Format.IgnoredFile file -> + logGrEqDetailed $"'%s{file}' was ignored" + return ProcessResult.Ignored inFile + | Format.FormatResult.Error(file, ex) -> return ProcessResult.Error(file, ex) + | Format.InvalidCode(file, _) -> + let ex = FormatException($"Formatting {file} lead to invalid F# code") + return ProcessResult.Error(file, ex) } - |> Async.RunSynchronously let private reportCheckResults (checkResult: Format.CheckResult) = checkResult.Errors @@ -147,13 +178,13 @@ let private reportCheckResults (checkResult: Format.CheckResult) = |> List.map (fun filename -> $"%s{filename} needs formatting") |> Seq.iter stdlog -let runCheckCommand (verbosity: VerbosityLevel) (recurse: bool) (inputPath: InputPath) : int = +let runCheckCommand (recurse: bool) (inputPath: InputPath) : int = let check files = Async.RunSynchronously(Format.checkCode files) let processCheckResult (checkResult: Format.CheckResult) = if checkResult.IsValid then - logGrEqDetailed verbosity "No changes required." + logGrEqDetailed "No changes required." 0 else reportCheckResults checkResult @@ -170,7 +201,7 @@ let runCheckCommand (verbosity: VerbosityLevel) (recurse: bool) (inputPath: Inpu elog "No input path provided. Call with --help for usage information." 1 | InputPath.File f when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> - logGrEqDetailed verbosity $"'%s{f}' was ignored" + logGrEqDetailed $"'%s{f}' was ignored" 0 | InputPath.File path -> path |> Seq.singleton |> check |> processCheckResult | InputPath.Folder path -> path |> allFiles recurse |> check |> processCheckResult @@ -243,17 +274,17 @@ let main argv = let version = results.TryGetResult <@ Arguments.Version @> - let verbosity = - let maybeVerbosity = - results.TryGetResult <@ Arguments.Verbosity @> - |> Option.map (fun v -> v.ToLowerInvariant()) + let maybeVerbosity = + results.TryGetResult <@ Arguments.Verbosity @> + |> Option.map (fun v -> v.ToLowerInvariant()) + let verbosity = match maybeVerbosity with | None | Some "n" - | Some "normal" -> VerbosityLevel.Normal + | Some "normal" -> initLogger VerbosityLevel.Normal | Some "d" - | Some "detailed" -> VerbosityLevel.Detailed + | Some "detailed" -> initLogger VerbosityLevel.Detailed | Some _ -> elog "Invalid verbosity level" exit 1 @@ -261,9 +292,9 @@ let main argv = AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> closeAndFlushLog ()) let fileToFile (force: bool) (inFile: string) (outFile: string) = - try - logGrEqDetailed verbosity $"Processing %s{inFile}" - let hasByteOrderMark = hasByteOrderMark inFile + async { + logGrEqDetailed $"Processing %s{inFile}" + let! hasByteOrderMark = hasByteOrderMark inFile use buffer = if hasByteOrderMark then @@ -274,46 +305,51 @@ let main argv = else new StreamWriter(outFile) - if profile then - File.ReadLines(inFile) |> Seq.length |> (fun l -> stdlog $"Line count: %i{l}") - - time (fun () -> processSourceFile verbosity force inFile buffer) - else - processSourceFile verbosity force inFile buffer + let! processResult = + if profile then + async { + let! length = File.ReadAllLinesAsync(inFile) |> Async.AwaitTask + length |> Seq.length |> (fun l -> stdlog $"Line count: %i{l}") + return! timeAsync (fun () -> processSourceFile force inFile buffer) + } + else + processSourceFile force inFile buffer - buffer.Flush() - logGrEqDetailed verbosity $"%s{outFile} has been written." - 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 + async { if profile then stdlog $"""Line count: %i{s.Length - s.Replace(Environment.NewLine, "").Length}""" - - time (fun () -> processSourceString verbosity force s outFile config) + return! timeAsync (fun () -> processSourceString force s outFile config) else - processSourceString verbosity force s outFile config - with exn -> - reraise () + return! processSourceString force s outFile config + } let processFile force inputFile outputFile = - if inputFile <> outputFile then - fileToFile force inputFile outputFile - else - logGrEqDetailed verbosity $"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 ProcessResult.Error(inputFile, e) + } let processFolder force inputFolder outputFolder = if not <| Directory.Exists(outputFolder) then Directory.CreateDirectory(outputFolder) |> ignore - let files = allFiles recurse inputFolder - - files - |> Seq.iter (fun i -> + allFiles recurse inputFolder + |> Seq.toList + |> List.map (fun i -> // s supposes to have form s1/suffix let suffix = i.Substring(inputFolder.Length + 1) @@ -325,27 +361,65 @@ let main argv = processFile force i o) - Seq.length files - - let filesAndFolders force (files: string list) (folders: string list) : int = - let singleFilesProcessed = + let filesAndFolders force (files: string list) (folders: string list) = + let fileTasks = files - |> List.sumBy (fun file -> + |> List.map (fun file -> if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then - logGrEqDetailed verbosity $"'%s{file}' was ignored" - 0 + logGrEqDetailed $"'%s{file}' was ignored" + async.Return(ProcessResult.Ignored(file)) else - processFile force file file - 1) + processFile force file file) - let filesInFoldersProcessed = - folders |> List.sumBy (fun folder -> processFolder force folder folder) + let folderTasks = + folders |> List.collect (fun folder -> processFolder force folder folder) - singleFilesProcessed + filesInFoldersProcessed + (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 + | ProcessResult.Formatted x -> (x :: oks, ignores, unchanged, errors) + | ProcessResult.Ignored i -> (oks, i :: ignores, unchanged, errors) + | ProcessResult.Unchanged u -> (oks, ignores, u :: unchanged, errors) + | ProcessResult.Error(file, e) -> (oks, ignores, unchanged, (file, e) :: errors)) + + let reportFormatResults (results: #seq) = + let oks, ignored, unchanged, errored = partitionResults results + + let summaryMessage = + $"Formatted: %d{oks.Length}, Ignored : %d{ignored.Length}, Unchanged : %d{unchanged.Length}, Errored: %d{errored.Length}" + + stdlog summaryMessage + + errored + |> Seq.iter (fun (file, ex) -> + let message = + match verbosity with + | VerbosityLevel.Normal -> + match ex with + | :? ParseException -> "Could not parse file." + | :? FormatException as fe -> fe.Message + | _ -> "" + | VerbosityLevel.Detailed -> $"%A{ex}" + + let message = + if String.IsNullOrEmpty message then + message + else + $" : {message}" + + elog $"Failed to format file: {file}{message}") + + if errored.Length > 0 then + exit 1 + + let asyncRunner = Async.Parallel >> Async.RunSynchronously + if Option.isSome version then let version = CodeFormatter.GetVersion() stdlog $"Fantomas v%s{version}" @@ -358,7 +432,7 @@ let main argv = daemon.WaitForClose.GetAwaiter().GetResult() exit 0 elif check then - inputPath |> runCheckCommand verbosity recurse |> exit + inputPath |> runCheckCommand recurse |> exit else try match inputPath, outputPath with @@ -372,18 +446,16 @@ let main argv = elog "Input path is missing. Call with --help for usage information." exit 1 | InputPath.File f, _ when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> - logGrEqDetailed verbosity $"'%s{f}' was ignored" + logGrEqDetailed $"'%s{f}' was ignored" | InputPath.Folder p1, OutputPath.NotKnown -> - let n = processFolder force p1 p1 - logGrEqDetailed verbosity $"Processed files: %d{n}" - | 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 -> - let n = processFolder force p1 p2 - logGrEqDetailed verbosity $"Processed files: %d{n}" + 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 -> - let n = filesAndFolders force files folders - logGrEqDetailed verbosity $"Processed files: %d{n}" + filesAndFolders force files folders |> asyncRunner |> reportFormatResults | InputPath.Multiple _, OutputPath.IO _ -> elog "Multiple input files are not supported with the --out flag." exit 1 From e528cc6d72873535c8d91c7d2dfcf5bf15c196d7 Mon Sep 17 00:00:00 2001 From: Alex Berezhnykh Date: Sat, 4 Feb 2023 15:46:15 +0100 Subject: [PATCH 13/34] Add separate CodeFormatter.FormatDocumentAsync overloads with cursor and config --- src/Fantomas.Core.Tests/CursorTests.fs | 36 ++++++++++---------------- src/Fantomas.Core/CodeFormatter.fs | 11 +++++--- src/Fantomas.Core/CodeFormatter.fsi | 18 ++++++++++--- src/Fantomas/Daemon.fs | 15 ++++++----- 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/Fantomas.Core.Tests/CursorTests.fs b/src/Fantomas.Core.Tests/CursorTests.fs index 23ce640b7b..550d5f0287 100644 --- a/src/Fantomas.Core.Tests/CursorTests.fs +++ b/src/Fantomas.Core.Tests/CursorTests.fs @@ -5,39 +5,31 @@ open NUnit.Framework open FsUnit open Fantomas.Core -let assertCursor (expectedLine: int, expectedColumn: int) (actualCursor: pos) : unit = - Assert.AreEqual(Position.mkPos expectedLine expectedColumn, actualCursor) +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`` () = - let source = + formatWithCursor """ let a = "foobar" """ - - let formattedResult = - CodeFormatter.FormatDocumentAsync(false, source, cursor = CodeFormatter.MakePosition(3, 8)) - |> Async.RunSynchronously - - // After formatting the let binding will be on one line - - match formattedResult.Cursor with - | None -> Assert.Fail "Expected a cursor" - | Some cursor -> assertCursor (1, 12) cursor + (3, 8) + |> assertCursor (1, 12) [] let ``cursor outside of a node`` () = - let source = + formatWithCursor """ let a = () """ - - let formattedResult = - CodeFormatter.FormatDocumentAsync(false, source, cursor = CodeFormatter.MakePosition(3, 7)) - |> Async.RunSynchronously - - match formattedResult.Cursor with - | None -> Assert.Fail "Expected a cursor" - | Some cursor -> assertCursor (1, 11) cursor + (3, 7) + |> assertCursor (1, 11) diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 9efa706e69..39d58e7042 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -25,9 +25,14 @@ type CodeFormatter = let sourceText = Some(CodeFormatterImpl.getSourceText source) CodeFormatterImpl.formatAST ast sourceText config None |> async.Return - static member FormatDocumentAsync(isSignature, source, ?config, ?cursor: Position) = - let config = Option.defaultValue FormatConfig.Default config - CodeFormatterImpl.formatDocument config isSignature (CodeFormatterImpl.getSourceText source) cursor + static member FormatDocumentAsync(isSignature, source) = + CodeFormatterImpl.formatDocument FormatConfig.Default isSignature (CodeFormatterImpl.getSourceText source) None + + static member FormatDocumentAsync(isSignature, source, 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 diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index e64a873959..ff8296487c 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -18,8 +18,20 @@ type CodeFormatter = /// Format an abstract syntax tree with the original source for trivia processing static member FormatASTAsync: ast: ParsedInput * source: string -> Async - /// Format an abstract syntax tree with the original source for trivia processing using a given config - static member FormatASTAsync: ast: ParsedInput * 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 + 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. @@ -29,7 +41,7 @@ type CodeFormatter = /// Fantomas configuration /// The location of a cursor, zero-based. static member FormatDocumentAsync: - isSignature: bool * source: string * ?config: FormatConfig * ?cursor: pos -> Async + 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. diff --git a/src/Fantomas/Daemon.fs b/src/Fantomas/Daemon.fs index f7793bd311..31cc92f45e 100644 --- a/src/Fantomas/Daemon.fs +++ b/src/Fantomas/Daemon.fs @@ -64,12 +64,15 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = try let! formatResponse = - CodeFormatter.FormatDocumentAsync( - request.IsSignatureFile, - request.SourceCode, - config, - ?cursor = cursor - ) + 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 From 9681ea5005ff5c6c9a1058c4b9a65078701a9a60 Mon Sep 17 00:00:00 2001 From: nojaf Date: Sat, 4 Feb 2023 16:08:11 +0100 Subject: [PATCH 14/34] Add release notes for 6.0.0-alpha-003. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 902d73c7d1..920482faeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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 From ea8af4795a49e55c2dbcbea2a3bf257f3318e66a Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Wed, 8 Feb 2023 01:36:46 -0700 Subject: [PATCH 15/34] Consolidate and share logic for Records and AnonymousRecords (#2750) * Remove AnonRecordFieldNode * Consolidate a lot of logic Introduce active pattern to join implementation for Record & AnonRecords Consolidate some more logic to fix an edge case Consolidate a bit more Remove unintentional change, cleanup Refactor to remove active pattern * Add ExprCopyableRecordNode subclass to better separate concerns * Clean up a bit * Remove a type, rename some others * Inline some one-off functions * Format CodePrinter.fs * Add some additional XML documentation. --------- Co-authored-by: nojaf --- .../CrampedMultilineBracketStyleTests.fs | 35 ++ src/Fantomas.Core/ASTTransformer.fs | 35 +- src/Fantomas.Core/CodePrinter.fs | 397 +++++++++--------- src/Fantomas.Core/Context.fs | 6 +- src/Fantomas.Core/Selection.fs | 9 +- src/Fantomas.Core/SyntaxOak.fs | 72 ++-- 6 files changed, 306 insertions(+), 248 deletions(-) diff --git a/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs index 6b0076a5c1..e2739ae0b1 100644 --- a/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs @@ -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 diff --git a/src/Fantomas.Core/ASTTransformer.fs b/src/Fantomas.Core/ASTTransformer.fs index ab4e21fcfd..c0d7d0f4de 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,20 +960,37 @@ 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(isStruct, copyInfo, recordFields, StartEndRange 2 (mOpen, _, mClose)) -> 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, + stn "{|" mOpen, Option.map (fst >> mkExpr creationAide) copyInfo, fields, stn "|}" mClose, diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 850b282116..168e028dae 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -461,198 +461,99 @@ let genExpr (e: Expr) = isSmallExpression size smallExpression multilineExpression ctx |> genNode node | Expr.Record node -> - let genRecordFieldName (node: RecordFieldNode) = - genIdentListNode node.FieldName - +> sepSpace - +> genSingleTextNode node.Equals - +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr - |> genNode node - - let fieldsExpr = col sepNln node.Fields genRecordFieldName - let hasFields = List.isNotEmpty node.Fields + let smallRecordExpr = genSmallRecordNode node + + let genCrampedFields targetColumn = + match node.CopyInfo with + | Some we -> genMultilineRecordCopyExpr (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 + // Lock the start of the record field, however keep potential indentations in relation to the opening curly brace + +> atCurrentColumn (genRecordFieldName e)) + ctx - 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 multilineRecordExpr = genMultilineRecord genCrampedFields node + genRecord smallRecordExpr multilineRecordExpr node + | Expr.AnonRecord node -> + let genStructPrefix = onlyIf node.IsStruct !- "struct " + let smallRecordExpr = genStructPrefix +> genSmallRecordNode node + + let genMultilineAnonCrampedFields targetColumn = + match node.CopyInfo with + | Some we -> + atCurrentColumn ( + genExpr we + +> (!- " with" +> indentSepNlnUnindent (genMultilineRecordFieldsExpr node)) + ) + | None -> + fun (ctx: Context) -> + col + sepNln + node.Fields + (fun fieldNode -> + let genNode = + if ctx.Config.IndentSize < 3 then + sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth + else + sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth + + // Add spaces to ensure the record field (incl trivia) starts at the right column. + addFixedSpaces targetColumn + +> atCurrentColumn (enterNode fieldNode +> genIdentListNode fieldNode.FieldName) + +> sepSpace + +> genSingleTextNode fieldNode.Equals + +> genNode (genExpr fieldNode.Expr) + +> leaveNode fieldNode) + ctx 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 - +> ifElseCtx - (fun ctx -> ctx.Config.IsStroustrupStyle) - (indent +> sepNln) - addSpaceIfSpaceAroundDelimiter - +> genCopyExpr fieldsExpr we - +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent - +> sepNln - +> genSingleTextNode node.ClosingBrace - | RecordNodeExtra.None -> - genSingleTextNode node.OpeningBrace - +> indentSepNlnUnindent fieldsExpr - +> ifElseCtx lastWriteEventIsNewline sepNone sepNln - +> genSingleTextNode node.ClosingBrace - - 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 - +> genCopyExpr fieldsExpr we - +> 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 + genStructPrefix +> genMultilineRecord genMultilineAnonCrampedFields 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 - ifAlignOrStroustrupBrackets genMultilineRecordInstanceAlignBrackets genMultilineRecordInstance + let genMultilineInheritRecordExpr = + let fieldsExpr = genMultilineRecordFieldsExpr node - 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 genInheritInfo = + (genSingleTextNode node.InheritConstructor.InheritKeyword + +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth (genInheritConstructor node.InheritConstructor) + |> genNode (InheritConstructor.Node node.InheritConstructor)) + +> onlyIf node.HasFields sepNln - 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 -> - genSingleTextNodeSuffixDelimiter node.OpeningBrace - +> ifElseCtx - (fun ctx -> ctx.Config.IsStroustrupStyle) - (indent +> sepNln) - sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> genCopyExpr fieldsExpr ci - +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent - +> sepNln - +> genSingleTextNode node.ClosingBrace - | None -> - genSingleTextNode node.OpeningBrace - +> indentSepNlnUnindent fieldsExpr - +> sepNln - +> genSingleTextNode node.ClosingBrace + let genMultilineAlignBrackets = + genSingleTextNode node.OpeningBrace + +> indentSepNlnUnindent (genInheritInfo +> fieldsExpr) + +> sepNln + +> genSingleTextNode node.ClosingBrace - ifElse node.IsStruct !- "struct " sepNone +> genAnonRecord + let genMultilineCramped = + genSingleTextNode node.OpeningBrace + +> addSpaceIfSpaceAroundDelimiter + +> atCurrentColumn ( + genInheritInfo + +> fieldsExpr + +> addSpaceIfSpaceAroundDelimiter + +> genSingleTextNode node.ClosingBrace + ) - 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 @@ -1717,8 +1618,22 @@ let genExpr (e: Expr) = | Expr.IndexFromEnd node -> !- "^" +> genExpr node.Expr |> genNode node | Expr.Typar node -> genSingleTextNode node -let genCopyExpr fieldsExpr ci = - atCurrentColumnIndent (genExpr ci) +let genQuoteExpr (node: ExprQuoteNode) = + genSingleTextNode node.OpenToken + +> sepSpace + +> expressionFitsOnRestOfLine (genExpr node.Expr) (indent +> sepNln +> genExpr node.Expr +> unindent +> sepNln) + +> sepSpace + +> genSingleTextNode node.CloseToken + |> genNode node + +/// +/// Prints the inside of an update record expression. +/// This function does not print the opening and closing braces. +/// +/// Record fields. +/// Expression before the `with` keyword. +let genMultilineRecordCopyExpr fieldsExpr copyExpr = + atCurrentColumnIndent (genExpr copyExpr) +> !- " with" +> indent +> whenShortIndent indent @@ -1727,14 +1642,116 @@ let genCopyExpr fieldsExpr ci = +> whenShortIndent unindent +> unindent -let genQuoteExpr (node: ExprQuoteNode) = - genSingleTextNode node.OpenToken - +> sepSpace - +> expressionFitsOnRestOfLine (genExpr node.Expr) (indent +> sepNln +> genExpr node.Expr +> unindent +> sepNln) +let genRecordFieldName (node: RecordFieldNode) = + genIdentListNode node.FieldName +> sepSpace - +> genSingleTextNode node.CloseToken + +> genSingleTextNode node.Equals + +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr |> genNode 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`. +/// +/// +/// +/// +/// Takes a targetColumn that indicates the column +/// after the opening brace `{ ` with respect to the `SpaceAroundDelimiter` setting. +/// +/// +/// In `Cramped` style we try to ensure that all record fields are starting at that column. +/// +/// +/// The ExprRecordNode +/// Context +let genMultilineRecord genCrampedFields (node: ExprRecordNode) (ctx: Context) = + let expressionStartColumn = ctx.Column + + let targetColumn = + let openBraceLength = node.OpeningBrace.Text.Length + + expressionStartColumn + + (if ctx.Config.SpaceAroundDelimiter then + openBraceLength + 1 + else + openBraceLength) + + let genMultilineAlignBrackets = + let fieldsExpr = genMultilineRecordFieldsExpr node + + match node.CopyInfo with + | Some ci -> + genSingleTextNodeSuffixDelimiter node.OpeningBrace + +> ifElseCtx + (fun ctx -> ctx.Config.IsStroustrupStyle) + (indent +> sepNln) + sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace + +> genMultilineRecordCopyExpr 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 = + match node.CopyInfo with + | Some _ -> + genSingleTextNode node.OpeningBrace + +> sepNlnWhenWriteBeforeNewlineNotEmptyOr addSpaceIfSpaceAroundDelimiter // comment after curly brace + +> genCrampedFields targetColumn + +> addSpaceIfSpaceAroundDelimiter + +> genSingleTextNode node.ClosingBrace + | None -> + atCurrentColumn ( + genSingleTextNodeSuffixDelimiter node.OpeningBrace + +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace + +> genCrampedFields targetColumn + +> 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 genMultilineFunctionApplicationArguments (argExpr: Expr) = let argsInsideParenthesis (parenNode: ExprParenNode) f = genSingleTextNode parenNode.OpeningParen diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 0b88cad3dd..1d0865fc91 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -756,11 +756,7 @@ let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = let isStroustrupEnabled = config.MultilineBracketStyle = Stroustrup match e with - | Expr.Record node -> - match node.Extra with - | RecordNodeExtra.Inherit _ -> false - | RecordNodeExtra.With _ - | RecordNodeExtra.None -> isStroustrupEnabled + | Expr.Record _ | Expr.AnonRecord _ | Expr.ArrayOrList _ -> isStroustrupEnabled | Expr.NamedComputation _ -> not config.NewlineBeforeMultilineComputationExpression diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index 27c79f6bce..b3d3ceddbe 100644 --- a/src/Fantomas.Core/Selection.fs +++ b/src/Fantomas.Core/Selection.fs @@ -163,12 +163,15 @@ let mkTreeWithSingleNode (node: Node) : TreeForSelection = | :? ExprArrayOrListNode as node -> let expr = Expr.ArrayOrList 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) + | :? ExprInheritRecordNode as node -> + let expr = Expr.InheritRecord node + mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) + | :? ExprRecordNode as node -> + let expr = Expr.Record node + mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) | :? ExprObjExprNode as node -> let expr = Expr.ObjExpr node mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index 2d01663f39..9cee838934 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -741,18 +741,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) @@ -761,57 +749,65 @@ 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 + 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 - -type AnonRecordFieldNode(ident: SingleTextNode, equals: SingleTextNode, rhs: Expr, range) = - inherit NodeBase(range) + member x.HasFields = List.isNotEmpty x.Fields - 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 - -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 |] +type ExprAnonRecordNode + ( + isStruct: bool, + openingBrace: SingleTextNode, + copyInfo: Expr option, + fields: RecordFieldNode list, + closingBrace: SingleTextNode, + range + ) = + inherit ExprRecordNode(openingBrace, copyInfo, fields, closingBrace, range) member val IsStruct = isStruct - member val OpeningBrace = openingBrace - member val CopyInfo = copyInfo - member val Fields = fields - member val ClosingBrace = closingBrace type InterfaceImplNode ( @@ -1608,6 +1604,7 @@ type Expr = | StructTuple of ExprStructTupleNode | ArrayOrList of ExprArrayOrListNode | Record of ExprRecordNode + | InheritRecord of ExprInheritRecordNode | AnonRecord of ExprAnonRecordNode | ObjExpr of ExprObjExprNode | While of ExprWhileNode @@ -1672,6 +1669,7 @@ type Expr = | StructTuple n -> n | ArrayOrList n -> n | Record n -> n + | InheritRecord n -> n | AnonRecord n -> n | ObjExpr n -> n | While n -> n From 77ca9404ecf37392085ddd57dc4f054d8aa9c433 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 10 Feb 2023 18:39:10 +0100 Subject: [PATCH 16/34] Add Spectr.Console (#2765) * Add Spectr.Console. * Remove unused CodeFormatException. * Update Fantomas.Tests * Continue on error * Some regex logic. * Remove duplicate ignore check. Print one-line if only one file was formatted. --- .github/workflows/main.yml | 2 +- .../Integration/IgnoreFilesTests.fs | 6 +- src/Fantomas.Tests/packages.lock.json | 9 +++ src/Fantomas/Fantomas.fsproj | 1 + src/Fantomas/Format.fs | 24 ------- src/Fantomas/Format.fsi | 2 - src/Fantomas/Program.fs | 67 +++++++++++++------ src/Fantomas/packages.lock.json | 9 +++ 8 files changed, 71 insertions(+), 49 deletions(-) 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/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs index 787cea8b10..8bb412c913 100644 --- a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs +++ b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs @@ -83,12 +83,12 @@ let ``ignore file in folder`` () = use ignoreFixture = new FantomasIgnoreFile("A.fs") let { ExitCode = exitCode; Output = output } = - runFantomasTool (sprintf "%s .%c%s" Verbosity Path.DirectorySeparatorChar subFolder) + runFantomasTool $"%s{Verbosity} .%c{Path.DirectorySeparatorChar}%s{subFolder}" exitCode |> should equal 0 File.ReadAllText inputFixture.Filename |> should equal Source - output |> should contain "Formatted: 0, Ignored : 0, Unchanged : 0, Errored: 0" + output |> should contain "A.fs was ignored" [] let ``ignore file while checking`` () = @@ -135,4 +135,4 @@ let ``honor ignore file when processing a folder`` () = runFantomasTool (sprintf "%s .%c%s" Verbosity Path.DirectorySeparatorChar subFolder) output |> should not' (contain "ignored") - output |> should contain "Formatted: 1, Ignored : 0, Unchanged : 0, Errored: 0" + output |> should contain "A.fs was formatted" diff --git a/src/Fantomas.Tests/packages.lock.json b/src/Fantomas.Tests/packages.lock.json index 398366087f..47f603d667 100644 --- a/src/Fantomas.Tests/packages.lock.json +++ b/src/Fantomas.Tests/packages.lock.json @@ -349,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", @@ -916,6 +924,7 @@ "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/Fantomas.fsproj b/src/Fantomas/Fantomas.fsproj index 65464c0356..6ad1b03514 100644 --- a/src/Fantomas/Fantomas.fsproj +++ b/src/Fantomas/Fantomas.fsproj @@ -45,5 +45,6 @@ + \ No newline at end of file diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index f6e983f2b7..c6a62b62ab 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -4,30 +4,6 @@ open System open System.IO open Fantomas.Core -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 FormatResult = | Formatted of filename: string * formattedContent: string | Unchanged of filename: string diff --git a/src/Fantomas/Format.fsi b/src/Fantomas/Format.fsi index 188be113d3..b5f8d6b49c 100644 --- a/src/Fantomas/Format.fsi +++ b/src/Fantomas/Format.fsi @@ -3,8 +3,6 @@ module Fantomas.Format open System open Fantomas.Core -exception CodeFormatException of (string * Option) array - type FormatResult = | Formatted of filename: string * formattedContent: string | Unchanged of filename: string diff --git a/src/Fantomas/Program.fs b/src/Fantomas/Program.fs index d896a645a8..0fbab0888e 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -6,6 +6,7 @@ open Fantomas.Daemon open Fantomas.Logging open Argu open System.Text +open Spectre.Console let extensions = set [| ".fs"; ".fsx"; ".fsi"; ".ml"; ".mli" |] @@ -68,6 +69,12 @@ type ProcessResult = | Unchanged of string | Error of string * exn +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) @@ -77,7 +84,7 @@ 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 allFiles isRec path = let searchOption = (if isRec then SearchOption.AllDirectories @@ -85,10 +92,7 @@ let rec allFiles isRec path = SearchOption.TopDirectoryOnly) 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 @@ -389,23 +393,15 @@ let main argv = | ProcessResult.Error(file, e) -> (oks, ignores, unchanged, (file, e) :: errors)) let reportFormatResults (results: #seq) = - let oks, ignored, unchanged, errored = partitionResults results - - let summaryMessage = - $"Formatted: %d{oks.Length}, Ignored : %d{ignored.Length}, Unchanged : %d{unchanged.Length}, Errored: %d{errored.Length}" - - stdlog summaryMessage - - errored - |> Seq.iter (fun (file, ex) -> + let reportError (file, exn: Exception) = let message = match verbosity with | VerbosityLevel.Normal -> - match ex with + match exn with | :? ParseException -> "Could not parse file." | :? FormatException as fe -> fe.Message | _ -> "" - | VerbosityLevel.Detailed -> $"%A{ex}" + | VerbosityLevel.Detailed -> $"%A{exn}" let message = if String.IsNullOrEmpty message then @@ -413,10 +409,43 @@ let main argv = else $" : {message}" - elog $"Failed to format file: {file}{message}") + elog $"Failed to format file: {file}{message}" - if errored.Length > 0 then - exit 1 + match Seq.tryExactlyOne results with + | Some singleResult -> + let fileName f = FileInfo(f).Name + + match singleResult with + | ProcessResult.Formatted f -> stdlog $"{fileName f} was formatted." + | ProcessResult.Ignored f -> stdlog $"{fileName f} was ignored." + | ProcessResult.Unchanged f -> stdlog $"{fileName f} was unchanged." + | ProcessResult.Error(f, e) -> + reportError (fileName f, e) + 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 + + if errored.Length > 0 then + exit 1 let asyncRunner = Async.Parallel >> Async.RunSynchronously diff --git a/src/Fantomas/packages.lock.json b/src/Fantomas/packages.lock.json index 168879c6be..5342a0e51f 100644 --- a/src/Fantomas/packages.lock.json +++ b/src/Fantomas/packages.lock.json @@ -72,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, )", From 691100cc458b7b261f789ee90657f005ae301314 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Sat, 11 Feb 2023 17:33:36 +0100 Subject: [PATCH 17/34] Consolidate records part two (#2766) * Remove genCrampedFields inside genMultilineRecord. * Remove ExprAnonRecordNode for now. * Expr.AnonStructRecord! --- ...dMultilineBracketStyleArrayOrListTests.fs} | 2 +- .../AlignedMultilineBracketStyleTests.fs | 25 ++++ .../CrampedMultilineBracketStyleTests.fs | 8 +- .../Fantomas.Core.Tests.fsproj | 3 +- .../NumberOfItemsRecordTests.fs | 4 +- .../Stroustrup/SynExprAnonRecdStructTests.fs | 35 ++++++ src/Fantomas.Core.Tests/StructTests.fs | 25 ++++ src/Fantomas.Core/ASTTransformer.fs | 33 ++++- src/Fantomas.Core/CodePrinter.fs | 119 +++++++----------- src/Fantomas.Core/Context.fs | 2 +- src/Fantomas.Core/Selection.fs | 6 +- src/Fantomas.Core/SyntaxOak.fs | 38 +++--- 12 files changed, 199 insertions(+), 101 deletions(-) rename src/Fantomas.Core.Tests/{MultilineBlockBracketsOnSameColumnArrayOrListTests.fs => AlignedMultilineBracketStyleArrayOrListTests.fs} (99%) create mode 100644 src/Fantomas.Core.Tests/Stroustrup/SynExprAnonRecdStructTests.fs diff --git a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs similarity index 99% rename from src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs rename to src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs index c5466a5f65..56f215d4e0 100644 --- a/src/Fantomas.Core.Tests/MultilineBlockBracketsOnSameColumnArrayOrListTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleArrayOrListTests.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.Tests.MultilineBlockBracketsOnSameColumnArrayOrListTests +module Fantomas.Core.Tests.AlignedMultilineBracketStyleArrayOrListTests open NUnit.Framework open FsUnit diff --git a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs index 9389d84b13..d2175c4b69 100644 --- a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs @@ -1545,3 +1545,28 @@ let v = { 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/CrampedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs index e2739ae0b1..59fb463bd1 100644 --- a/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs @@ -1721,8 +1721,8 @@ let ``record with comments above field, indent 2`` () = equal """ { Foo = - // bar - someValue } + // bar + someValue } """ [] @@ -1778,8 +1778,8 @@ let ``anonymous record with multiline field, indent 2`` () = equal """ {| Foo = - // meh - someValue |} + // meh + someValue |} """ [] diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index ec59e55cdf..bcbfc70b3f 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -59,7 +59,7 @@ - + @@ -108,6 +108,7 @@ + diff --git a/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs index 730d6e4135..c03f892f58 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsRecordTests.fs @@ -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/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/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/ASTTransformer.fs b/src/Fantomas.Core/ASTTransformer.fs index c0d7d0f4de..5d0bcb954a 100644 --- a/src/Fantomas.Core/ASTTransformer.fs +++ b/src/Fantomas.Core/ASTTransformer.fs @@ -975,7 +975,11 @@ let mkExpr (creationAide: CreationAide) (e: SynExpr) : Expr = | None, None -> ExprRecordNode(stn "{" mOpen, None, fieldNodes, stn "}" mClose, exprRange) |> Expr.Record - | SynExpr.AnonRecd(isStruct, copyInfo, recordFields, StartEndRange 2 (mOpen, _, mClose)) -> + | SynExpr.AnonRecd(true, + copyInfo, + recordFields, + (StartRange 6 (mStruct, _) & EndRange 2 (mClose, _)), + { OpeningBraceRange = mOpen }) -> let fields = recordFields |> List.choose (function @@ -988,15 +992,36 @@ let mkExpr (creationAide: CreationAide) (e: SynExpr) : Expr = Some(RecordFieldNode(longIdent, stn "=" mEq, mkExpr creationAide e, m)) | _ -> None) - ExprAnonRecordNode( - isStruct, + 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/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 168e028dae..1dd84d557b 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -462,58 +462,17 @@ let genExpr (e: Expr) = |> genNode node | Expr.Record node -> let smallRecordExpr = genSmallRecordNode node - - let genCrampedFields targetColumn = - match node.CopyInfo with - | Some we -> genMultilineRecordCopyExpr (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 - // Lock the start of the record field, however keep potential indentations in relation to the opening curly brace - +> atCurrentColumn (genRecordFieldName e)) - ctx - - let multilineRecordExpr = genMultilineRecord genCrampedFields node + let multilineRecordExpr = genMultilineRecord node genRecord smallRecordExpr multilineRecordExpr node - | Expr.AnonRecord node -> - let genStructPrefix = onlyIf node.IsStruct !- "struct " + | Expr.AnonStructRecord node -> + let genStructPrefix = genSingleTextNodeWithSpaceSuffix sepSpace node.Struct let smallRecordExpr = genStructPrefix +> genSmallRecordNode node - let genMultilineAnonCrampedFields targetColumn = - match node.CopyInfo with - | Some we -> - atCurrentColumn ( - genExpr we - +> (!- " with" +> indentSepNlnUnindent (genMultilineRecordFieldsExpr node)) - ) - | None -> - fun (ctx: Context) -> - col - sepNln - node.Fields - (fun fieldNode -> - let genNode = - if ctx.Config.IndentSize < 3 then - sepSpaceOrDoubleIndentAndNlnIfExpressionExceedsPageWidth - else - sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidth - - // Add spaces to ensure the record field (incl trivia) starts at the right column. - addFixedSpaces targetColumn - +> atCurrentColumn (enterNode fieldNode +> genIdentListNode fieldNode.FieldName) - +> sepSpace - +> genSingleTextNode fieldNode.Equals - +> genNode (genExpr fieldNode.Expr) - +> leaveNode fieldNode) - ctx - let multilineRecordExpr = - genStructPrefix +> genMultilineRecord genMultilineAnonCrampedFields node + if node.Struct.HasContentAfter then + genStructPrefix +> indentSepNlnUnindent (genMultilineRecord node) + else + genStructPrefix +> genMultilineRecord node genRecord smallRecordExpr multilineRecordExpr node | Expr.InheritRecord node -> @@ -873,7 +832,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 @@ -1630,24 +1589,28 @@ let genQuoteExpr (node: ExprQuoteNode) = /// 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 fieldsExpr copyExpr = +let genMultilineRecordCopyExpr (addAdditionalIndent: bool) fieldsExpr copyExpr = atCurrentColumnIndent (genExpr copyExpr) +> !- " with" +> indent - +> whenShortIndent indent + +> onlyIf addAdditionalIndent indent +> sepNln +> fieldsExpr - +> whenShortIndent unindent + +> onlyIf addAdditionalIndent unindent +> unindent let genRecordFieldName (node: RecordFieldNode) = - genIdentListNode node.FieldName - +> sepSpace - +> genSingleTextNode node.Equals + atCurrentColumn ( + enterNode node + +> genIdentListNode node.FieldName + +> sepSpace + +> genSingleTextNode node.Equals + ) +> sepSpaceOrIndentAndNlnIfExpressionExceedsPageWidthUnlessStroustrup genExpr node.Expr - |> genNode node + +> leaveNode node let genMultilineRecordFieldsExpr (node: ExprRecordBaseNode) = col sepNln node.Fields genRecordFieldName @@ -1680,23 +1643,13 @@ let genSmallRecordNode (node: ExprRecordNode) = /// This is too avoid offset errors when using a smaller `indent_size`. /// /// -/// -/// -/// Takes a targetColumn that indicates the column -/// after the opening brace `{ ` with respect to the `SpaceAroundDelimiter` setting. -/// -/// -/// In `Cramped` style we try to ensure that all record fields are starting at that column. -/// -/// /// The ExprRecordNode /// Context -let genMultilineRecord genCrampedFields (node: ExprRecordNode) (ctx: Context) = +let genMultilineRecord (node: ExprRecordNode) (ctx: Context) = let expressionStartColumn = ctx.Column + let openBraceLength = node.OpeningBrace.Text.Length let targetColumn = - let openBraceLength = node.OpeningBrace.Text.Length - expressionStartColumn + (if ctx.Config.SpaceAroundDelimiter then openBraceLength + 1 @@ -1708,12 +1661,14 @@ let genMultilineRecord genCrampedFields (node: ExprRecordNode) (ctx: Context) = 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 fieldsExpr ci + +> genMultilineRecordCopyExpr additionalIndent fieldsExpr ci +> onlyIfCtx (fun ctx -> ctx.Config.IsStroustrupStyle) unindent +> sepNln +> genSingleTextNode node.ClosingBrace @@ -1724,18 +1679,40 @@ let genMultilineRecord genCrampedFields (node: ExprRecordNode) (ctx: Context) = +> 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 - +> genCrampedFields targetColumn + +> genFields +> addSpaceIfSpaceAroundDelimiter +> genSingleTextNode node.ClosingBrace | None -> atCurrentColumn ( genSingleTextNodeSuffixDelimiter node.OpeningBrace +> sepNlnWhenWriteBeforeNewlineNotEmpty // comment after curly brace - +> genCrampedFields targetColumn + +> genFields +> sepNlnWhenWriteBeforeNewlineNotEmpty +> (fun ctx -> // Edge case scenario to make sure that the closing brace is not before the opening one diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 1d0865fc91..b995093dfc 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -757,7 +757,7 @@ let isStroustrupStyleExpr (config: FormatConfig) (e: Expr) = match e with | Expr.Record _ - | Expr.AnonRecord _ + | Expr.AnonStructRecord _ | Expr.ArrayOrList _ -> isStroustrupEnabled | Expr.NamedComputation _ -> not config.NewlineBeforeMultilineComputationExpression | _ -> false diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index b3d3ceddbe..c94d03ee67 100644 --- a/src/Fantomas.Core/Selection.fs +++ b/src/Fantomas.Core/Selection.fs @@ -163,12 +163,12 @@ let mkTreeWithSingleNode (node: Node) : TreeForSelection = | :? ExprArrayOrListNode as node -> let expr = Expr.ArrayOrList node mkOakFromModuleDecl (ModuleDecl.DeclExpr expr) - | :? ExprAnonRecordNode as node -> - let expr = Expr.AnonRecord 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) diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index 9cee838934..b6bb744b1e 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -759,6 +759,9 @@ type ExprRecordBaseNode(openingBrace: SingleTextNode, fields: RecordFieldNode li 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, @@ -779,35 +782,42 @@ type ExprRecordNode member x.HasFields = List.isNotEmpty x.Fields -type ExprInheritRecordNode +type ExprAnonStructRecordNode ( + structNode: SingleTextNode, openingBrace: SingleTextNode, - inheritConstructor: InheritConstructor, + copyInfo: Expr option, fields: RecordFieldNode list, closingBrace: SingleTextNode, range ) = - inherit ExprRecordBaseNode(openingBrace, fields, closingBrace, range) - - member val InheritConstructor = inheritConstructor + inherit ExprRecordNode(openingBrace, copyInfo, fields, closingBrace, range) + member val Struct = structNode override val Children: Node array = - [| yield openingBrace - yield InheritConstructor.Node inheritConstructor + [| 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, + inheritConstructor: InheritConstructor, fields: RecordFieldNode list, closingBrace: SingleTextNode, range ) = - inherit ExprRecordNode(openingBrace, copyInfo, fields, closingBrace, range) - member val IsStruct = isStruct + inherit ExprRecordBaseNode(openingBrace, fields, closingBrace, range) + + member val InheritConstructor = inheritConstructor + + override val Children: Node array = + [| yield openingBrace + yield InheritConstructor.Node inheritConstructor + yield! nodes fields + yield closingBrace |] type InterfaceImplNode ( @@ -1605,7 +1615,7 @@ type Expr = | ArrayOrList of ExprArrayOrListNode | Record of ExprRecordNode | InheritRecord of ExprInheritRecordNode - | AnonRecord of ExprAnonRecordNode + | AnonStructRecord of ExprAnonStructRecordNode | ObjExpr of ExprObjExprNode | While of ExprWhileNode | For of ExprForNode @@ -1670,7 +1680,7 @@ type Expr = | ArrayOrList n -> n | Record n -> n | InheritRecord n -> n - | AnonRecord n -> n + | AnonStructRecord n -> n | ObjExpr n -> n | While n -> n | For n -> n From 73fa7d778576f6b51a73f4910d77e10b892dd84e Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Sun, 12 Feb 2023 20:12:50 +0100 Subject: [PATCH 18/34] Upgrade guide (#2767) * Update dotnet tools. * First draft of the upgrade guide. * Update docs/docs/end-users/UpgradeGuide.md Co-authored-by: dawe * Fix navigation. --------- Co-authored-by: dawe --- .config/dotnet-tools.json | 4 +- build.fsx | 2 +- docs/docs/end-users/FAQ.md | 2 +- docs/docs/end-users/GeneratingCode.fsx | 2 +- docs/docs/end-users/UpgradeGuide.md | 77 ++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 docs/docs/end-users/UpgradeGuide.md 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/build.fsx b/build.fsx index 31113411f9..df8576f89d 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" 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..d79c39c0c6 100644 --- a/docs/docs/end-users/GeneratingCode.fsx +++ b/docs/docs/end-users/GeneratingCode.fsx @@ -318,5 +318,5 @@ Relying on these projects, is at your own risk. The constructed AST might not be 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. - + *) diff --git a/docs/docs/end-users/UpgradeGuide.md b/docs/docs/end-users/UpgradeGuide.md new file mode 100644 index 0000000000..ef7fc7636f --- /dev/null +++ b/docs/docs/end-users/UpgradeGuide.md @@ -0,0 +1,77 @@ +--- +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. + +### console application +- `-v` is now short for `--verbosity` instead of `--version` +- The console output was revamped. + +### Miscellaneous +- The public API of CodeFormatter no longer uses `FSharpOption<'T>`, instead overloads are now used. + + From 770e361b85bf3ab0e76f8e843c53fa8d2698dda6 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Tue, 14 Feb 2023 09:06:26 +0100 Subject: [PATCH 19/34] Update GeneratingCode.fsx to use the Oak model. (#2769) * Update GeneratingCode.fsx to use the Oak model. * Remove unnecessary code. * Apply suggestions from code review Co-authored-by: dawe --------- Co-authored-by: dawe --- docs/docs/end-users/GeneratingCode.fsx | 289 ++++++++----------------- docs/images/ast-viewer.png | Bin 67093 -> 0 bytes docs/images/oak-viewer.png | Bin 0 -> 67093 bytes docs/images/searchbar-ast.png | Bin 38800 -> 47803 bytes 4 files changed, 91 insertions(+), 198 deletions(-) delete mode 100644 docs/images/ast-viewer.png create mode 100644 docs/images/oak-viewer.png diff --git a/docs/docs/end-users/GeneratingCode.fsx b/docs/docs/end-users/GeneratingCode.fsx index d79c39c0c6..5acf6a3484 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([], 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/images/ast-viewer.png b/docs/images/ast-viewer.png deleted file mode 100644 index 07b6ef7e3e05286591627c1e26850ac9aaf7d522..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67093 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 literal 0 HcmV?d00001 diff --git a/docs/images/searchbar-ast.png b/docs/images/searchbar-ast.png index d44c129ac6abaa96c74e5bfba0fffac6ca545a09..6b18ecbb82063f82c2606865b0703b521e99a104 100644 GIT binary patch literal 47803 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 literal 38800 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 Date: Sat, 18 Feb 2023 10:18:18 +0100 Subject: [PATCH 20/34] Always process folder recursive. (#2775) * Always process folder recursive. * Remove last traces of recurse. Add UpgradeGuide.md entry. --- docs/docs/end-users/UpgradeGuide.md | 1 + .../Integration/IgnoreFilesTests.fs | 2 +- .../Integration/MultiplePathsTests.fs | 4 ++-- src/Fantomas/Program.fs | 24 +++++++------------ tests/regressions.fsx | 2 +- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/docs/docs/end-users/UpgradeGuide.md b/docs/docs/end-users/UpgradeGuide.md index ef7fc7636f..e7828a14b8 100644 --- a/docs/docs/end-users/UpgradeGuide.md +++ b/docs/docs/end-users/UpgradeGuide.md @@ -70,6 +70,7 @@ fsharp_experimental_stroustrup_style = true ### 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. diff --git a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs index 8bb412c913..384c5cc096 100644 --- a/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs +++ b/src/Fantomas.Tests/Integration/IgnoreFilesTests.fs @@ -53,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 diff --git a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs index 2806389c19..dbcf657fcc 100644 --- a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs +++ b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs @@ -38,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) @@ -48,7 +48,7 @@ let ``format multiple paths with recursive flag`` () = use fileFixtureThree = new TemporaryFileCodeSample(UserCode, subFolder = "sub") let arguments = - sprintf "%s \"%s\" \"%s\" \"sub\" -r" Verbosity 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/Program.fs b/src/Fantomas/Program.fs index 0fbab0888e..8c90b699b7 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -11,7 +11,6 @@ open Spectre.Console let extensions = set [| ".fs"; ".fsx"; ".fsi"; ".ml"; ".mli" |] type Arguments = - | [] Recurse | [] Force | [] Profile | [] Out of string @@ -24,7 +23,6 @@ type Arguments = 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." @@ -83,13 +81,9 @@ let isInExcludedDir (fullPath: string) = let isFSharpFile (s: string) = Set.contains (Path.GetExtension s) extensions -/// Get all appropriate files, either recursively or non-recursively -let 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)) @@ -182,7 +176,7 @@ let private reportCheckResults (checkResult: Format.CheckResult) = |> 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) @@ -208,12 +202,12 @@ let runCheckCommand (recurse: bool) (inputPath: InputPath) : int = 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 @@ -274,8 +268,6 @@ 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 = @@ -351,7 +343,7 @@ let main argv = if not <| Directory.Exists(outputFolder) then Directory.CreateDirectory(outputFolder) |> ignore - allFiles recurse inputFolder + findAllFilesRecursively inputFolder |> Seq.toList |> List.map (fun i -> // s supposes to have form s1/suffix @@ -461,7 +453,7 @@ 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 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 { From 82ffbafb13a890bf7199a6fef65ff0cbae1a12a4 Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 20 Feb 2023 10:29:16 +0100 Subject: [PATCH 21/34] Move profile logic to Format.fs and use Spectre (#2770) * Move profile logic to Format.fs and use Spectre * Rename ProfileInfos -> ProfileInfo Don't touch Fantomas.Core * Reuse the oks and unchanged lists from partitionResults for the call to reportProfileInfos * Show shorter time format. * Bundle parameters to formatContentAsync and formatFileAsync in a single record FormatParams * Move profile logic to Format.fs and use Spectre * Rename ProfileInfos -> ProfileInfo Don't touch Fantomas.Core * Remove ProcessResult and use FormatResult for everything * centralize exception creation and fix wording * Final nits --------- Co-authored-by: nojaf --- src/Fantomas/Format.fs | 229 +++++++++++++++++++++++++--------------- src/Fantomas/Format.fsi | 40 ++++--- src/Fantomas/Program.fs | 156 ++++++++++++++------------- 3 files changed, 248 insertions(+), 177 deletions(-) diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index c6a62b62ab..8eae2e903d 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -1,115 +1,168 @@ -module Fantomas.Format +namespace Fantomas open System open System.IO open Fantomas.Core +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! { Code = 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 false + let formatContentAsync = formatContentInternalAsync -let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: string) = - let config = EditorConfig.readConfiguration file + let private formatFileInternalAsync (parms: FormatParams) = + if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) parms.File then + async { return IgnoredFile parms.File } + else - if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then - async { return IgnoredFile file } - else + async { + let! originalContent = File.ReadAllTextAsync parms.File |> Async.AwaitTask - async { - let! originalContent = File.ReadAllTextAsync file |> Async.AwaitTask + 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 b5f8d6b49c..0e338f89b0 100644 --- a/src/Fantomas/Format.fsi +++ b/src/Fantomas/Format.fsi @@ -1,18 +1,25 @@ -module Fantomas.Format +namespace Fantomas open System open Fantomas.Core +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: (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 @@ -24,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/Program.fs b/src/Fantomas/Program.fs index 8c90b699b7..7852593b30 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -37,15 +37,6 @@ type Arguments = (Seq.map (fun s -> "*" + s) extensions |> String.concat ",") | Verbosity _ -> "Set the verbosity level. Allowed values are n[ormal] and d[etailed]." -let timeAsync f = - async { - let sw = Diagnostics.Stopwatch.StartNew() - let! res = f () - sw.Stop() - stdlog $"Time taken: %O{sw.Elapsed} s" - return res - } - [] type InputPath = | File of string @@ -60,13 +51,6 @@ type OutputPath = | IO of string | NotKnown -[] -type ProcessResult = - | Formatted of string - | Ignored of string - | Unchanged of string - | Error of string * exn - type Table with member x.SetBorder(border: TableBorder) = @@ -104,8 +88,11 @@ let private hasByteOrderMark file = 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) = async { let! hasBom = hasByteOrderMark fileName @@ -119,55 +106,56 @@ let processSourceString (force: bool) s (fileName: string) config = } 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) -> + | FormatResult.Formatted(_, formattedContent, _) as r -> do! formattedContent |> writeResult - return ProcessResult.Formatted(fileName) - | Format.InvalidCode(file, formattedContent) when force -> + return r + | FormatResult.InvalidCode(file, formattedContent) when force -> stdlog $"%s{file} was not valid after formatting." do! formattedContent |> writeResult - return ProcessResult.Formatted(fileName) - | Format.FormatResult.Unchanged file -> + return FormatResult.Formatted(fileName, formattedContent, None) + | FormatResult.Unchanged(file, _) as r -> logGrEqDetailed $"'%s{file}' was unchanged" - return ProcessResult.Unchanged(fileName) - | Format.IgnoredFile file -> + return r + | FormatResult.IgnoredFile file as r -> logGrEqDetailed $"'%s{file}' was ignored" - return ProcessResult.Ignored fileName - | Format.FormatResult.Error(file, ex) -> return ProcessResult.Error(file, ex) - | Format.InvalidCode(file, _) -> - let ex = FormatException($"Formatting {file} lead to invalid F# code") - return ProcessResult.Error(file, ex) + return r + | FormatResult.Error _ as r -> return r + | FormatResult.InvalidCode(file, _) -> + let ex = invalidResultException file + return FormatResult.Error(file, ex) } /// 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) -> + | FormatResult.Formatted(_, formattedContent, _) as r -> do! tw.WriteAsync(formattedContent) |> Async.AwaitTask - return ProcessResult.Formatted(inFile) - | Format.InvalidCode(file, formattedContent) when force -> + return r + | FormatResult.InvalidCode(file, formattedContent) when force -> stdlog $"%s{file} was not valid after formatting." do! tw.WriteAsync(formattedContent) |> Async.AwaitTask - return ProcessResult.Formatted(inFile) - | Format.FormatResult.Unchanged _ -> + return FormatResult.Formatted(inFile, formattedContent, None) + | FormatResult.Unchanged _ as r -> let! input = inFile |> File.ReadAllTextAsync |> Async.AwaitTask do! input |> tw.WriteAsync |> Async.AwaitTask - return ProcessResult.Unchanged inFile - | Format.IgnoredFile file -> + return r + | FormatResult.IgnoredFile file as r -> logGrEqDetailed $"'%s{file}' was ignored" - return ProcessResult.Ignored inFile - | Format.FormatResult.Error(file, ex) -> return ProcessResult.Error(file, ex) - | Format.InvalidCode(file, _) -> - let ex = FormatException($"Formatting {file} lead to invalid F# code") - return ProcessResult.Error(file, ex) + return r + | FormatResult.Error _ as r -> return r + | FormatResult.InvalidCode(file, _) -> + let ex = invalidResultException file + return FormatResult.Error(file, ex) } -let private reportCheckResults (checkResult: Format.CheckResult) = +let private reportCheckResults (checkResult: CheckResult) = checkResult.Errors |> List.map (fun (filename, exn) -> $"error: Failed to format %s{filename}: %s{exn.ToString()}") |> Seq.iter elog @@ -180,7 +168,7 @@ 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 logGrEqDetailed "No changes required." 0 @@ -301,15 +289,7 @@ let main argv = else new StreamWriter(outFile) - let! processResult = - if profile then - async { - let! length = File.ReadAllLinesAsync(inFile) |> Async.AwaitTask - length |> Seq.length |> (fun l -> stdlog $"Line count: %i{l}") - return! timeAsync (fun () -> processSourceFile force inFile buffer) - } - else - processSourceFile force inFile buffer + let! processResult = processSourceFile force profile inFile buffer do! buffer.FlushAsync() |> Async.AwaitTask logGrEqDetailed $"%s{outFile} has been written." @@ -317,13 +297,7 @@ let main argv = } let stringToFile (force: bool) (s: string) (outFile: string) config = - async { - if profile then - stdlog $"""Line count: %i{s.Length - s.Replace(Environment.NewLine, "").Length}""" - return! timeAsync (fun () -> processSourceString force s outFile config) - else - return! processSourceString force s outFile config - } + async { return! processSourceString force profile s outFile config } let processFile force inputFile outputFile = async { @@ -336,7 +310,7 @@ let main argv = let config = EditorConfig.readConfiguration inputFile return! stringToFile force content inputFile config with e -> - return ProcessResult.Error(inputFile, e) + return FormatResult.Error(inputFile, e) } let processFolder force inputFolder outputFolder = @@ -363,7 +337,7 @@ let main argv = |> List.map (fun file -> if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then logGrEqDetailed $"'%s{file}' was ignored" - async.Return(ProcessResult.Ignored(file)) + async.Return(FormatResult.IgnoredFile(file)) else processFile force file file) @@ -375,16 +349,19 @@ let main argv = let check = results.Contains <@ Arguments.Check @> let isDaemon = results.Contains <@ Arguments.Daemon @> - let partitionResults (results: #seq) = + let partitionResults (results: #seq) = (([], [], [], []), results) ||> Seq.fold (fun (oks, ignores, unchanged, errors) next -> match next with - | ProcessResult.Formatted x -> (x :: oks, ignores, unchanged, errors) - | ProcessResult.Ignored i -> (oks, i :: ignores, unchanged, errors) - | ProcessResult.Unchanged u -> (oks, ignores, u :: unchanged, errors) - | ProcessResult.Error(file, e) -> (oks, ignores, unchanged, (file, e) :: errors)) - - let reportFormatResults (results: #seq) = + | 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 @@ -403,17 +380,44 @@ let main argv = 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 - | ProcessResult.Formatted f -> stdlog $"{fileName f} was formatted." - | ProcessResult.Ignored f -> stdlog $"{fileName f} was ignored." - | ProcessResult.Unchanged f -> stdlog $"{fileName f} was unchanged." - | ProcessResult.Error(f, e) -> + | 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() @@ -436,6 +440,8 @@ let main argv = for e in errored do reportError e + reportProfileInfos (oks @ unchanged) + if errored.Length > 0 then exit 1 From 91253aac68755fb9c74d37e7e1b12ad68e01fc75 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Wed, 22 Feb 2023 09:42:03 +0100 Subject: [PATCH 22/34] Don't hook up SerilogTraceListener in FantomasDaemon. (#2777) --- src/Fantomas.Tests/Fantomas.Tests.fsproj | 1 + src/Fantomas.Tests/ToolLocatorTests.fs | 25 ++++++++++++++++++++++++ src/Fantomas/Daemon.fs | 9 +++++---- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/Fantomas.Tests/ToolLocatorTests.fs diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index dbddd3d7c6..836424e727 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/ToolLocatorTests.fs b/src/Fantomas.Tests/ToolLocatorTests.fs new file mode 100644 index 0000000000..72c27d3c66 --- /dev/null +++ b/src/Fantomas.Tests/ToolLocatorTests.fs @@ -0,0 +1,25 @@ +module Fantomas.CoreGlobalTool.Tests.ToolLocatorTests + +open Fantomas.Client +open Fantomas.Client.LSPFantomasServiceTypes +open Fantomas.Client.Contracts +open NUnit.Framework + +[] +[] +let ``locate fantomas tool`` () = + let pwd = @"C:\Users\nojaf\Projects" + let result = FantomasToolLocator.findFantomasTool (Folder pwd) + + match result with + | Error error -> Assert.Fail $"Could not locate tool: %A{error}" + | Ok(FantomasToolFound(FantomasVersion(version), startInfo)) -> + let result = FantomasToolLocator.createFor startInfo + + match result with + | Error error -> Assert.Fail $"Could not start tool: %A{error}" + | Ok runningFantomasTool -> + let version2 = + runningFantomasTool.RpcClient.InvokeAsync(Methods.Version).Result + + Assert.AreEqual(version, $"v{version2}") diff --git a/src/Fantomas/Daemon.fs b/src/Fantomas/Daemon.fs index 31cc92f45e..f8169b28aa 100644 --- a/src/Fantomas/Daemon.fs +++ b/src/Fantomas/Daemon.fs @@ -16,13 +16,12 @@ 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) @@ -33,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 From 0a84d8265bf8d34ecd3be71e2e60ce9d87ef8b0d Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 22 Feb 2023 09:47:31 +0100 Subject: [PATCH 23/34] Add release notes for 6.0.0-alpha-004. --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 920482faeb..e6faac9e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [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 From 5ea3fdb7faeb2019e45c78fd82b609c639e2a39d Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Thu, 23 Feb 2023 00:32:35 -0700 Subject: [PATCH 24/34] Fix record type generation for stroustrup types with members (#2773) * Fix record type generation for stroustrup types with members * Reuse with keyword if it exists, add tests --- .../AlignedMultilineBracketStyleTests.fs | 23 +++++++ .../CrampedMultilineBracketStyleTests.fs | 21 ++++++ .../SynTypeDefnSigReprSimpleTests.fs | 14 ++-- .../SynTypeDefnSimpleReprRecordTests.fs | 65 +++++++++++++++---- src/Fantomas.Core/CodePrinter.fs | 42 +++++++----- 5 files changed, 124 insertions(+), 41 deletions(-) diff --git a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs index d2175c4b69..8752786b19 100644 --- a/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/AlignedMultilineBracketStyleTests.fs @@ -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 diff --git a/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs index 59fb463bd1..4d22cff4e6 100644 --- a/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs +++ b/src/Fantomas.Core.Tests/CrampedMultilineBracketStyleTests.fs @@ -1076,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 diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs index 3fdb9c16eb..5edcc57e36 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSigReprSimpleTests.fs @@ -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 fa6cbce5aa..f6fb4713cc 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs @@ -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 diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 1dd84d557b..533eb6d012 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -3023,7 +3023,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 :: _ -> @@ -3152,7 +3152,7 @@ let genTypeDefn (td: TypeDefn) = +> indentSepNlnUnindent ( col sepNln node.EnumCases genEnumCase +> onlyIf hasMembers sepNln - +> sepNlnTypeAndMembers typeDefnNode + +> sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members ) |> genNode node @@ -3185,11 +3185,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 = @@ -3200,7 +3201,7 @@ let genTypeDefn (td: TypeDefn) = let genMembers = onlyIf hasMembers sepNln - +> sepNlnTypeAndMembers typeDefnNode + +> sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members let anyFieldHasXmlDoc = @@ -3212,8 +3213,16 @@ 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 let cramped = sepNlnUnlessLastEventIsNewline @@ -3223,19 +3232,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 - | Stroustrup when hasNoMembers -> stroustrupWithoutMembers ctx - | Aligned - | Stroustrup -> 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 @@ -3247,14 +3255,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.IsStroustrupStyle && hasNoMembers then + if ctx.Config.IsStroustrupStyle then (sepSpace +> short) ctx else isSmallExpression size short (indentSepNlnUnindent short) ctx @@ -3309,7 +3315,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 From 0c73517ac3a63626272ce1c669a03136d288c869 Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Thu, 23 Feb 2023 00:36:34 -0700 Subject: [PATCH 25/34] Fix handling of AppExpr with a single stroustrup record (#2747) * Fix handling of AppExpr with a single stroustrup record * Add some more tests * Refactor to handle record applications after other args * Add specific settings for new feature * Fix rebase issue * Remove extra setting for stroustrup record application expressions * Fix rebase issues --- .../SynBindingValueExpressionTests.fs | 150 ++++++++++++++++++ src/Fantomas.Core/CodePrinter.fs | 51 +++++- src/Fantomas.Core/Context.fs | 2 +- src/Fantomas.Core/Context.fsi | 2 +- 4 files changed, 197 insertions(+), 8 deletions(-) diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs index 654c191c57..0ca525fdaa 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynBindingValueExpressionTests.fs @@ -569,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/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 533eb6d012..163f593e5a 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -1178,6 +1178,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 = @@ -1236,7 +1261,7 @@ let genExpr (e: Expr) = clauseNode.WhenExpr +> sepSpace +> genSingleTextNodeWithSpaceSuffix sepSpace clauseNode.Arrow - +> indentSepNlnUnindentExprUnlessStroustrup genExpr clauseNode.BodyExpr + +> indentSepNlnUnindentUnlessStroustrup genExpr clauseNode.BodyExpr +> leaveNode clauseNode atCurrentColumn ( @@ -1822,7 +1847,7 @@ let genNamedArgumentExpr (node: ExprInfixAppNode) = genExpr node.LeftHandSide +> sepSpace +> genSingleTextNode node.Operator - +> indentSepNlnUnindentExprUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide + +> indentSepNlnUnindentUnlessStroustrup (fun e -> sepSpace +> genExpr e) node.RightHandSide expressionFitsOnRestOfLine short long |> genNode node @@ -2154,6 +2179,22 @@ let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = 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 + + visit appNode.Arguments + let genAppWithLambda sep (node: ExprAppWithLambdaNode) = let short = genExpr node.FunctionName @@ -2683,7 +2724,7 @@ let genBinding (b: BindingNode) (ctx: Context) : Context = let short = sepSpace +> body let long = - indentSepNlnUnindentExprUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr + indentSepNlnUnindentUnlessStroustrup (fun e -> sepSpace +> genExpr e) b.Expr isShortExpression ctx.Config.MaxFunctionBindingWidth short long @@ -2772,9 +2813,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 +> indentSepNlnUnindentExprUnlessStroustrup genExpr b.Expr - + let long = prefix +> indentSepNlnUnindentUnlessStroustrup genExpr b.Expr isShortExpression ctx.Config.MaxValueBindingWidth short long ctx) genNode b binding ctx diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index b995093dfc..7513f90606 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -928,7 +928,7 @@ let addParenIfAutoNln expr f = let expr = f expr expressionFitsOnRestOfLine expr (ifElse hasParenthesis (sepOpenT +> expr +> sepCloseT) expr) -let indentSepNlnUnindentExprUnlessStroustrup f (e: Expr) (ctx: Context) = +let indentSepNlnUnindentUnlessStroustrup f (e: Expr) (ctx: Context) = let shouldUseStroustrup = isStroustrupStyleExpr ctx.Config e && canSafelyUseStroustrup (Expr.Node e) diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index 7bb4b8db3a..80e4d4921e 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -257,7 +257,7 @@ val sepSpaceUnlessWriteBeforeNewlineNotEmpty: ctx: Context -> Context val autoIndentAndNlnWhenWriteBeforeNewlineNotEmpty: f: (Context -> Context) -> ctx: Context -> Context val addParenIfAutoNln: expr: Expr -> f: (Expr -> Context -> Context) -> (Context -> Context) -val indentSepNlnUnindentExprUnlessStroustrup: 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 From d96f01803ccf17323ecddf13cded617af33b7105 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 24 Feb 2023 14:16:34 +0100 Subject: [PATCH 26/34] Manually deserialize FormatDocumentResponse. (#2780) * Manually deserialize FormatDocumentResponse. * Apply suggestions from code review Co-authored-by: dawe --------- Co-authored-by: dawe --- docs/docs/contributors/Solution Structure.md | 6 +- fantomas.sln | 14 + src/Fantomas.Client.Tests/EndToEndTests.fs | 107 +++ .../Fantomas.Client.Tests.fsproj | 27 + src/Fantomas.Client.Tests/packages.lock.json | 819 ++++++++++++++++++ src/Fantomas.Client/CHANGELOG.md | 3 + src/Fantomas.Client/LSPFantomasService.fs | 103 ++- .../LSPFantomasServiceTypes.fs | 27 - .../LSPFantomasServiceTypes.fsi | 2 - src/Fantomas.Tests/Fantomas.Tests.fsproj | 2 +- src/Fantomas.Tests/FantomasServiceTests.fs | 31 + src/Fantomas.Tests/ToolLocatorTests.fs | 25 - 12 files changed, 1107 insertions(+), 59 deletions(-) create mode 100644 src/Fantomas.Client.Tests/EndToEndTests.fs create mode 100644 src/Fantomas.Client.Tests/Fantomas.Client.Tests.fsproj create mode 100644 src/Fantomas.Client.Tests/packages.lock.json create mode 100644 src/Fantomas.Tests/FantomasServiceTests.fs delete mode 100644 src/Fantomas.Tests/ToolLocatorTests.fs 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/fantomas.sln b/fantomas.sln index f0e27002b7..455820b02b 100644 --- a/fantomas.sln +++ b/fantomas.sln @@ -41,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 @@ -135,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.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 0bfa8043d1..142bc3a776 100644 --- a/src/Fantomas.Client/CHANGELOG.md +++ b/src/Fantomas.Client/CHANGELOG.md @@ -2,6 +2,9 @@ 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) diff --git a/src/Fantomas.Client/LSPFantomasService.fs b/src/Fantomas.Client/LSPFantomasService.fs index f348324cdc..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 @@ -236,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 @@ -243,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 = @@ -274,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 diff --git a/src/Fantomas.Client/LSPFantomasServiceTypes.fs b/src/Fantomas.Client/LSPFantomasServiceTypes.fs index bbc5ec6d8d..7505e3b22d 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fs +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fs @@ -45,33 +45,6 @@ type FormatDocumentResponse = | Error of filename: string * formattingError: string | IgnoredFile of filename: string - member this.AsFormatResponse() = - match this with - | FormatDocumentResponse.Formatted(name, content, cursor) -> - { Code = int FantomasResponseCode.Formatted - FilePath = name - Content = Some content - SelectedRange = None - Cursor = cursor } - | FormatDocumentResponse.Unchanged name -> - { Code = int FantomasResponseCode.UnChanged - FilePath = name - Content = None - SelectedRange = None - Cursor = None } - | FormatDocumentResponse.Error(name, err) -> - { Code = int FantomasResponseCode.Error - FilePath = name - Content = Some(err) - SelectedRange = None - Cursor = None } - | FormatDocumentResponse.IgnoredFile name -> - { Code = int FantomasResponseCode.Ignored - FilePath = name - Content = None - SelectedRange = None - Cursor = 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 bf346ee3b9..8dedde8b4e 100644 --- a/src/Fantomas.Client/LSPFantomasServiceTypes.fsi +++ b/src/Fantomas.Client/LSPFantomasServiceTypes.fsi @@ -29,8 +29,6 @@ type FormatDocumentResponse = | 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.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index 836424e727..96ed543379 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -27,7 +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/ToolLocatorTests.fs b/src/Fantomas.Tests/ToolLocatorTests.fs deleted file mode 100644 index 72c27d3c66..0000000000 --- a/src/Fantomas.Tests/ToolLocatorTests.fs +++ /dev/null @@ -1,25 +0,0 @@ -module Fantomas.CoreGlobalTool.Tests.ToolLocatorTests - -open Fantomas.Client -open Fantomas.Client.LSPFantomasServiceTypes -open Fantomas.Client.Contracts -open NUnit.Framework - -[] -[] -let ``locate fantomas tool`` () = - let pwd = @"C:\Users\nojaf\Projects" - let result = FantomasToolLocator.findFantomasTool (Folder pwd) - - match result with - | Error error -> Assert.Fail $"Could not locate tool: %A{error}" - | Ok(FantomasToolFound(FantomasVersion(version), startInfo)) -> - let result = FantomasToolLocator.createFor startInfo - - match result with - | Error error -> Assert.Fail $"Could not start tool: %A{error}" - | Ok runningFantomasTool -> - let version2 = - runningFantomasTool.RpcClient.InvokeAsync(Methods.Version).Result - - Assert.AreEqual(version, $"v{version2}") From 392f8db7e61170c95ad11849ab96aa3887b78be0 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 24 Feb 2023 14:23:46 +0100 Subject: [PATCH 27/34] Add release notes for 6.0.0-alpha-005. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6faac9e03..742c64931a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [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 From 118a6663b2f3d68e4e2ef8987f67763103fdaf62 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Mon, 6 Mar 2023 18:56:57 +0100 Subject: [PATCH 28/34] Extract Stroustrup for final list argument into a separate setting. (#2781) * Extract Stroustrup for final list argument into a separate setting. * Rename genArray to genArrayOrList. --- docs/docs/end-users/Configuration.fsx | 39 +++ .../Fantomas.Core.Tests.fsproj | 3 +- .../NumberOfItemsListOrArrayTests.fs | 8 +- ...ishTests.fs => FinalListArgumentsTests.fs} | 222 ++++++------------ .../FunctionApplicationDualListTests.fs | 2 +- .../FunctionApplicationSingleListTests.fs | 2 +- .../Stroustrup/NamedParameterTests.fs | 155 ++++++++++++ src/Fantomas.Core/CodePrinter.fs | 173 +++++++------- src/Fantomas.Core/FormatConfig.fs | 5 + .../EditorConfigurationTests.fs | 19 ++ 10 files changed, 387 insertions(+), 241 deletions(-) rename src/Fantomas.Core.Tests/Stroustrup/{ElmishTests.fs => FinalListArgumentsTests.fs} (84%) create mode 100644 src/Fantomas.Core.Tests/Stroustrup/NamedParameterTests.fs diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index a205f1f6f1..ec7201eb07 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -868,6 +868,45 @@ formatCode KeepMaxNumberOfBlankLines = 1 } (*** include-output ***) +(** + + +Applies the Stroustrup style to the final (two) array or list argument(s) in a function application. + +Default = false +*) + +formatCode + """ +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 + StroustrupFinalListArguments = true } +(*** include-output ***) + (** diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index bcbfc70b3f..93ff9148eb 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -105,10 +105,11 @@ - + + diff --git a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs index 6d4caa649a..f35ce891f0 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs @@ -88,7 +88,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } |> prepend newline |> should equal @@ -206,7 +206,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } |> prepend newline |> should equal @@ -240,7 +240,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } |> prepend newline |> should equal @@ -272,7 +272,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs similarity index 84% rename from src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs rename to src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs index 692a4c36cd..b426a3c817 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/ElmishTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.Tests.ElmishTests +module Fantomas.Core.Tests.Stroustrup.FinalListArgumentsTests open NUnit.Framework open FsUnit @@ -7,152 +7,7 @@ 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 - ) - ] - ) - ) - ] - ) - ) - ) -""" + StroustrupFinalListArguments = 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 @@ -1528,3 +1381,70 @@ 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 ] +""" diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs index e45b9cd5f7..0d54c925b0 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } [] let ``two short lists`` () = diff --git a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs index 5a7d4b6111..4c04072b99 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - MultilineBracketStyle = Stroustrup } + StroustrupFinalListArguments = true } [] let ``short function application`` () = 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/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 163f593e5a..3aafbf5fd4 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -393,73 +393,7 @@ 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 smallRecordExpr = genSmallRecordNode node let multilineRecordExpr = genMultilineRecord node @@ -1105,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 @@ -1134,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 @@ -1162,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 @@ -1170,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 @@ -1754,6 +1694,74 @@ 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 @@ -2147,7 +2155,7 @@ let genFunctionNameWithMultilineLids (trailing: Context -> Context) (longIdent: |> genNode parentNode let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.IsStroustrupStyle then + if not config.StroustrupFinalListArguments then None else let mutable otherArgs = ListCollector() @@ -2155,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 @@ -2164,7 +2171,7 @@ let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = visit appNode.Arguments let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.IsStroustrupStyle then + if not config.StroustrupFinalListArguments then None else let mutable otherArgs = ListCollector() @@ -2172,7 +2179,7 @@ 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 diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index c1c329d811..b91bbdfaee 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -225,6 +225,10 @@ type FormatConfig = [] NewlineBeforeMultilineComputationExpression: bool + [] + [] + StroustrupFinalListArguments: bool + [] [] [] @@ -269,4 +273,5 @@ type FormatConfig = MultilineBracketStyle = Cramped KeepMaxNumberOfBlankLines = 100 NewlineBeforeMultilineComputationExpression = true + StroustrupFinalListArguments = false StrictMode = false } diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 1cde1e3b9a..1282da8b61 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -522,3 +522,22 @@ fsharp_newline_before_multiline_computation_expression = false let config = EditorConfig.readConfiguration fsharpFile.FSharpFile Assert.IsFalse config.NewlineBeforeMultilineComputationExpression + +[] +let fsharp_stroustrup_final_list_arguments () = + let rootDir = tempName () + + let editorConfig = + """ +[*.fs] +fsharp_stroustrup_final_list_arguments = true +""" + + use configFixture = + new ConfigurationFile(defaultConfig, rootDir, content = editorConfig) + + use fsharpFile = new FSharpFile(rootDir) + + let config = EditorConfig.readConfiguration fsharpFile.FSharpFile + + Assert.IsTrue config.StroustrupFinalListArguments From 5a020e3df2ae239dec8572a618d2c9f72df72f3f Mon Sep 17 00:00:00 2001 From: Josh DeGraw <18509575+josh-degraw@users.noreply.github.com> Date: Tue, 7 Mar 2023 09:50:04 -0700 Subject: [PATCH 29/34] Fix Stroustrup record member declaration indent issue (#2788) * Fix issue with Stroustrup member indents * Move unindent closer to indent --- .../SynTypeDefnSimpleReprRecordTests.fs | 60 +++++++++++++++++++ src/Fantomas.Core/CodePrinter.fs | 7 +-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs index f6fb4713cc..a3b9e8e528 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/SynTypeDefnSimpleReprRecordTests.fs @@ -294,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/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 3aafbf5fd4..39c70aa259 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -3246,9 +3246,7 @@ let genTypeDefn (td: TypeDefn) = +> genSingleTextNode node.ClosingBrace let genMembers = - onlyIf hasMembers sepNln - +> sepNlnBetweenTypeAndMembers typeDefnNode - +> genMemberDefnList members + onlyIf hasMembers (sepNln +> sepNlnBetweenTypeAndMembers typeDefnNode +> genMemberDefnList members) let anyFieldHasXmlDoc = List.exists (fun (fieldNode: FieldNode) -> fieldNode.XmlDoc.IsSome) node.Fields @@ -3267,8 +3265,7 @@ let genTypeDefn (td: TypeDefn) = genAccessOpt node.Accessibility +> genRecordFields - +> onlyIf hasMembers (sepSpace +> withKw +> indent) - +> genMembers + +> onlyIf hasMembers (sepSpace +> withKw +> indent +> genMembers +> unindent) let cramped = sepNlnUnlessLastEventIsNewline From 76be1b9a6a094ddc606c7da333e808750a4d19ba Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 17 Mar 2023 18:14:38 +0100 Subject: [PATCH 30/34] EndsWithSingleListApp & EndsWithDualListApp are enable by IsStroustrupStyle as well. (#2795) * EndsWithSingleListApp & EndsWithDualListApp are enbled by both ExperimentalElmish or IsStroustrupStyle. * Renamed test file. --- docs/docs/end-users/Configuration.fsx | 7 +- .../Fantomas.Core.Tests.fsproj | 2 +- .../NumberOfItemsListOrArrayTests.fs | 8 +-- ...ntsTests.fs => ExperimentalElmishTests.fs} | 71 ++++++++++++++++++- .../FunctionApplicationDualListTests.fs | 2 +- .../FunctionApplicationSingleListTests.fs | 2 +- src/Fantomas.Core/CodePrinter.fs | 4 +- src/Fantomas.Core/FormatConfig.fs | 4 +- .../EditorConfigurationTests.fs | 4 +- 9 files changed, 86 insertions(+), 18 deletions(-) rename src/Fantomas.Core.Tests/Stroustrup/{FinalListArgumentsTests.fs => ExperimentalElmishTests.fs} (96%) diff --git a/docs/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index ec7201eb07..837da6e679 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -869,9 +869,10 @@ formatCode (*** include-output ***) (** - + -Applies the Stroustrup style to the final (two) array or list argument(s) in a function application. +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`. Default = false *) @@ -904,7 +905,7 @@ let singleList = ] """ { FormatConfig.Default with - StroustrupFinalListArguments = true } + ExperimentalElmish = true } (*** include-output ***) (** diff --git a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index 93ff9148eb..053f70a02e 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -109,7 +109,7 @@ - + diff --git a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs index f35ce891f0..a0d3fefdda 100644 --- a/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs +++ b/src/Fantomas.Core.Tests/NumberOfItemsListOrArrayTests.fs @@ -88,7 +88,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - StroustrupFinalListArguments = true } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -206,7 +206,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - StroustrupFinalListArguments = true } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -240,7 +240,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - StroustrupFinalListArguments = true } + ExperimentalElmish = true } |> prepend newline |> should equal @@ -272,7 +272,7 @@ h [ longValueThatIsALotOfCharactersSoooooLong; longValueThatIsALotOfCharactersSo """ { config with ArrayOrListMultilineFormatter = NumberOfItems - StroustrupFinalListArguments = true } + ExperimentalElmish = true } |> prepend newline |> should equal diff --git a/src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs b/src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs similarity index 96% rename from src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs rename to src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs index b426a3c817..5e7d871f49 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FinalListArgumentsTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/ExperimentalElmishTests.fs @@ -1,4 +1,4 @@ -module Fantomas.Core.Tests.Stroustrup.FinalListArgumentsTests +module Fantomas.Core.Tests.Stroustrup.ExperimentalElmishTests open NUnit.Framework open FsUnit @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - StroustrupFinalListArguments = true } + ExperimentalElmish = true } [] let ``input without attributes`` () = @@ -1448,3 +1448,70 @@ let stillCramped = 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 0d54c925b0..86e6578a99 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationDualListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - StroustrupFinalListArguments = true } + 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 4c04072b99..9b54989810 100644 --- a/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs +++ b/src/Fantomas.Core.Tests/Stroustrup/FunctionApplicationSingleListTests.fs @@ -7,7 +7,7 @@ open Fantomas.Core let config = { config with - StroustrupFinalListArguments = true } + ExperimentalElmish = true } [] let ``short function application`` () = diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 39c70aa259..0e2eb0d0ed 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -2155,7 +2155,7 @@ let genFunctionNameWithMultilineLids (trailing: Context -> Context) (longIdent: |> genNode parentNode let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.StroustrupFinalListArguments then + if not (config.ExperimentalElmish || config.IsStroustrupStyle) then None else let mutable otherArgs = ListCollector() @@ -2171,7 +2171,7 @@ let (|EndsWithDualListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = visit appNode.Arguments let (|EndsWithSingleListApp|_|) (config: FormatConfig) (appNode: ExprAppNode) = - if not config.StroustrupFinalListArguments then + if not (config.ExperimentalElmish || config.IsStroustrupStyle) then None else let mutable otherArgs = ListCollector() diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index b91bbdfaee..10d8ce2cb5 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -227,7 +227,7 @@ type FormatConfig = [] [] - StroustrupFinalListArguments: bool + ExperimentalElmish: bool [] [] @@ -273,5 +273,5 @@ type FormatConfig = MultilineBracketStyle = Cramped KeepMaxNumberOfBlankLines = 100 NewlineBeforeMultilineComputationExpression = true - StroustrupFinalListArguments = false + ExperimentalElmish = false StrictMode = false } diff --git a/src/Fantomas.Tests/EditorConfigurationTests.fs b/src/Fantomas.Tests/EditorConfigurationTests.fs index 1282da8b61..cc60848d0d 100644 --- a/src/Fantomas.Tests/EditorConfigurationTests.fs +++ b/src/Fantomas.Tests/EditorConfigurationTests.fs @@ -530,7 +530,7 @@ let fsharp_stroustrup_final_list_arguments () = let editorConfig = """ [*.fs] -fsharp_stroustrup_final_list_arguments = true +fsharp_experimental_elmish = true """ use configFixture = @@ -540,4 +540,4 @@ fsharp_stroustrup_final_list_arguments = true let config = EditorConfig.readConfiguration fsharpFile.FSharpFile - Assert.IsTrue config.StroustrupFinalListArguments + Assert.IsTrue config.ExperimentalElmish From e8106b12bb2d8589d5ea5927e98af3d6ca0541dc Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 17 Mar 2023 18:23:29 +0100 Subject: [PATCH 31/34] Add release notes for 6.0.0-alpha-006. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 742c64931a..b319d4dcd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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 From 1aba34c0180bcd9d04f686dcb1e2e09a94fa89fb Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Sat, 18 Mar 2023 13:49:54 +0100 Subject: [PATCH 32/34] Remove strict mode (#2798) * Remove StrictMode from Config * Only clean the Release folder. * Fix typo in UpgradeGuide.md --- build.fsx | 16 +++++----- docs/docs/end-users/Configuration.fsx | 26 ----------------- docs/docs/end-users/UpgradeGuide.md | 2 ++ src/Fantomas.Core.Tests/ExternTests.fs | 4 +-- .../InterpolatedStringTests.fs | 4 +-- src/Fantomas.Core.Tests/StringTests.fs | 8 ++--- src/Fantomas.Core.Tests/SynConstTests.fs | 4 +-- src/Fantomas.Core.Tests/SynLongIdentTests.fs | 1 - src/Fantomas.Core.Tests/TestHelpers.fs | 29 ++++++++++--------- src/Fantomas.Core.Tests/UnionTests.fs | 4 +-- src/Fantomas.Core/CodeFormatter.fs | 4 +-- src/Fantomas.Core/CodeFormatterImpl.fs | 2 +- src/Fantomas.Core/CodePrinter.fs | 6 ++-- src/Fantomas.Core/Context.fs | 8 +++-- src/Fantomas.Core/Context.fsi | 15 ++++++---- src/Fantomas.Core/FormatConfig.fs | 10 ++----- src/Fantomas.Core/Selection.fs | 2 +- 17 files changed, 63 insertions(+), 82 deletions(-) diff --git a/build.fsx b/build.fsx index df8576f89d..4726a68d60 100644 --- a/build.fsx +++ b/build.fsx @@ -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/docs/end-users/Configuration.fsx b/docs/docs/end-users/Configuration.fsx index 837da6e679..7b90fe74fc 100644 --- a/docs/docs/end-users/Configuration.fsx +++ b/docs/docs/end-users/Configuration.fsx @@ -908,32 +908,6 @@ let singleList = ExperimentalElmish = true } (*** 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. - -**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. -*) - -formatCode - """ - // some great comment - let add a b = - #if INTERACTIVE - 42 - #else - a + b - #endif - """ - { FormatConfig.Default with - StrictMode = true } -(*** include-output ***) (** diff --git a/docs/docs/end-users/UpgradeGuide.md b/docs/docs/end-users/UpgradeGuide.md index e7828a14b8..8847d0ca07 100644 --- a/docs/docs/end-users/UpgradeGuide.md +++ b/docs/docs/end-users/UpgradeGuide.md @@ -66,6 +66,7 @@ fsharp_experimental_stroustrup_style = true - `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` @@ -74,5 +75,6 @@ fsharp_experimental_stroustrup_style = true ### 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/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/InterpolatedStringTests.fs b/src/Fantomas.Core.Tests/InterpolatedStringTests.fs index c5f333c88b..e3b20c67b1 100644 --- a/src/Fantomas.Core.Tests/InterpolatedStringTests.fs +++ b/src/Fantomas.Core.Tests/InterpolatedStringTests.fs @@ -63,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/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/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 d4cfc3b519..7e0cb150fd 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 562011c84c..4c65d65e2f 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -25,15 +25,24 @@ 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}" + 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! formatted = CodeFormatter.FormatASTAsync(ast, config = config) let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted.Code) if not isValid then @@ -41,7 +50,6 @@ let formatSourceString isFsiFile (s: string) config = return formatted.Code.Replace("\r\n", "\n") } - |> Async.RunSynchronously let formatSourceStringWithDefines defines (s: string) config = @@ -83,11 +91,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/UnionTests.fs b/src/Fantomas.Core.Tests/UnionTests.fs index c3d2c99be5..9315d9e66d 100644 --- a/src/Fantomas.Core.Tests/UnionTests.fs +++ b/src/Fantomas.Core.Tests/UnionTests.fs @@ -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/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 39d58e7042..69d1648da6 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -66,14 +66,14 @@ type CodeFormatter = static member FormatOakAsync(oak: Oak) : Async = async { - let context = Context.Context.Create FormatConfig.Default + 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 config + 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/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 1ddaf29829..85edd8cd41 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -57,7 +57,7 @@ let formatAST (config: FormatConfig) (cursor: pos option) : FormatResult = - let context = Context.Context.Create config + let context = Context.Context.Create sourceText.IsSome config let oak = match sourceText with diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 0e2eb0d0ed..1e9711bf94 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -1510,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 @@ -1520,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 -> diff --git a/src/Fantomas.Core/Context.fs b/src/Fantomas.Core/Context.fs index 7513f90606..c7876b4907 100644 --- a/src/Fantomas.Core/Context.fs +++ b/src/Fantomas.Core/Context.fs @@ -181,6 +181,7 @@ module WriterEvents = [] type Context = { Config: FormatConfig + HasSource: bool WriterModel: WriterModel WriterEvents: Queue FormattedCursor: pos option } @@ -188,12 +189,15 @@ type Context = /// Initialize with a string writer and use space as delimiter static member Default = { Config = FormatConfig.Default + HasSource = false WriterModel = WriterModel.init 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 diff --git a/src/Fantomas.Core/Context.fsi b/src/Fantomas.Core/Context.fsi index 80e4d4921e..d54c6938c6 100644 --- a/src/Fantomas.Core/Context.fsi +++ b/src/Fantomas.Core/Context.fsi @@ -53,14 +53,19 @@ type WriterModel = [] type Context = - { Config: FormatConfig - WriterModel: WriterModel - WriterEvents: Queue - FormattedCursor: pos option } + { + 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 diff --git a/src/Fantomas.Core/FormatConfig.fs b/src/Fantomas.Core/FormatConfig.fs index 10d8ce2cb5..e2abad7247 100644 --- a/src/Fantomas.Core/FormatConfig.fs +++ b/src/Fantomas.Core/FormatConfig.fs @@ -227,12 +227,7 @@ type FormatConfig = [] [] - ExperimentalElmish: bool - - [] - [] - [] - StrictMode: bool } + ExperimentalElmish: bool } member x.IsStroustrupStyle = x.MultilineBracketStyle = Stroustrup @@ -273,5 +268,4 @@ type FormatConfig = MultilineBracketStyle = Cramped KeepMaxNumberOfBlankLines = 100 NewlineBeforeMultilineComputationExpression = true - ExperimentalElmish = false - StrictMode = false } + ExperimentalElmish = false } diff --git a/src/Fantomas.Core/Selection.fs b/src/Fantomas.Core/Selection.fs index c94d03ee67..1faca42e7a 100644 --- a/src/Fantomas.Core/Selection.fs +++ b/src/Fantomas.Core/Selection.fs @@ -402,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 -> From 17deeccd2b1032dccc164ebc985e49d8ff2850a0 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Sun, 19 Mar 2023 18:56:59 +0100 Subject: [PATCH 33/34] Cursor with defines (#2774) * Refactor processing of multiple defines into separate module. * Improve correctness of the behavior. * Fix remaining Util tests. * Format MultipleDefineCombinations.fs * Apply suggestions from code review Co-authored-by: dawe * Remove redundant elif * Implement Equals and GetHashCode. --------- Co-authored-by: dawe --- src/Fantomas.Core.Tests/CursorTests.fs | 11 + src/Fantomas.Core.Tests/DefinesTests.fs | 2 +- .../Fantomas.Core.Tests.fsproj | 1 + .../MultipleDefineCombinationsTests.fs | 194 ++++++++++++ src/Fantomas.Core.Tests/TestHelpers.fs | 13 +- src/Fantomas.Core.Tests/UtilsTests.fs | 116 ------- src/Fantomas.Core/CodeFormatter.fs | 7 +- src/Fantomas.Core/CodeFormatterImpl.fs | 45 +-- src/Fantomas.Core/Defines.fs | 117 +++---- src/Fantomas.Core/Defines.fsi | 15 +- src/Fantomas.Core/Fantomas.Core.fsproj | 2 + .../MultipleDefineCombinations.fs | 297 ++++++++++++++++++ .../MultipleDefineCombinations.fsi | 6 + src/Fantomas.Core/SyntaxOak.fs | 2 - src/Fantomas.Core/Trivia.fs | 6 +- src/Fantomas.Core/Utils.fs | 67 +--- src/Fantomas.Core/Utils.fsi | 10 +- src/Fantomas.Core/Validation.fs | 2 +- 18 files changed, 629 insertions(+), 284 deletions(-) create mode 100644 src/Fantomas.Core.Tests/MultipleDefineCombinationsTests.fs create mode 100644 src/Fantomas.Core/MultipleDefineCombinations.fs create mode 100644 src/Fantomas.Core/MultipleDefineCombinations.fsi diff --git a/src/Fantomas.Core.Tests/CursorTests.fs b/src/Fantomas.Core.Tests/CursorTests.fs index 550d5f0287..08d645e1e0 100644 --- a/src/Fantomas.Core.Tests/CursorTests.fs +++ b/src/Fantomas.Core.Tests/CursorTests.fs @@ -33,3 +33,14 @@ 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/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/Fantomas.Core.Tests.fsproj b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj index 053f70a02e..b0cb713268 100644 --- a/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj +++ b/src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj @@ -125,6 +125,7 @@ + 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/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index 4c65d65e2f..8dabb54a45 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -62,7 +62,7 @@ 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 @@ -70,14 +70,13 @@ let formatSourceStringWithDefines defines (s: string) config = } |> 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.Code ] - |> 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 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/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index 69d1648da6..cdd70e714b 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -7,7 +7,10 @@ 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) : Async = CodeFormatterImpl.formatAST ast None FormatConfig.Default None |> async.Return @@ -61,7 +64,7 @@ type CodeFormatter = ast |> Array.map (fun (ast, defines) -> let oak = ASTTransformer.mkOak (Some sourceText) ast - oak, defines) + oak, defines.Value) } static member FormatOakAsync(oak: Oak) : Async = diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index 85edd8cd41..c7ee04addb 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -4,7 +4,8 @@ module internal Fantomas.Core.CodeFormatterImpl open FSharp.Compiler.Diagnostics open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open Fantomas.Core.SyntaxOak +open SyntaxOak +open MultipleDefineCombinations let getSourceText (source: string) : ISourceText = source.TrimEnd() |> SourceText.ofString @@ -28,7 +29,7 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin if not errors.IsEmpty then raise (ParseException baseDiagnostics) - return [| (baseUntypedTree, []) |] + return [| (baseUntypedTree, DefineCombination.Empty) |] } | hashDirectives -> let defineCombinations = Defines.getDefineCombination hashDirectives @@ -37,7 +38,7 @@ 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 @@ -96,43 +97,7 @@ let formatDocument match results with | [] -> failwith "not possible" | [ (_, x) ] -> x - | all -> - // TODO: we currently ignore the cursor here. - // We would need to know which defines provided the code for each fragment. - // If we have a cursor, we need to find the fragment that contains it and matches the defines of the cursor. - - let allInFragments = - all - |> List.map (fun (dc, { Code = code }) -> dc, code) - |> 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.""" - ) - ) - - let mergedCode = - List.map snd allInFragments - |> List.reduce String.merge - |> String.concat config.EndOfLine.NewLineString - - { Code = mergedCode; Cursor = None } + | all -> mergeMultipleFormatResults config all return merged } 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 4dfd93c7c3..35aa74aaed 100644 --- a/src/Fantomas.Core/Fantomas.Core.fsproj +++ b/src/Fantomas.Core/Fantomas.Core.fsproj @@ -30,6 +30,8 @@ + + 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/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index b6bb744b1e..4ff3c525b9 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -3,8 +3,6 @@ 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 diff --git a/src/Fantomas.Core/Trivia.fs b/src/Fantomas.Core/Trivia.fs index dace6d4b8b..da4678425c 100644 --- a/src/Fantomas.Core/Trivia.fs +++ b/src/Fantomas.Core/Trivia.fs @@ -187,7 +187,6 @@ let rec visitLastChildNode (node: Node) : Node = | :? ExprLetOrUseBangNode | :? ExprAndBang | :? BindingNode - | :? ModuleOrNamespaceNode | :? TypeDefnEnumNode | :? TypeDefnUnionNode | :? TypeDefnRecordNode @@ -227,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) 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 From 21b11d34d3c0c6e26ea3f610184feec234087def Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Mon, 27 Mar 2023 11:06:34 +0200 Subject: [PATCH 34/34] Return string from FormatASTAsync api. (#2799) * Return string from FormatASTAsync api. * Add identifier in code sample. * Add release notes. --- CHANGELOG.md | 11 +++++++ docs/docs/end-users/GeneratingCode.fsx | 2 +- src/Fantomas.Core.Tests/FormatAstTests.fs | 1 - src/Fantomas.Core.Tests/SynLongIdentTests.fs | 1 - src/Fantomas.Core.Tests/TestHelpers.fs | 9 +++--- src/Fantomas.Core/CodeFormatter.fs | 32 +++++++++++++------- src/Fantomas.Core/CodeFormatter.fsi | 6 ++-- src/Fantomas.Core/CodeFormatterImpl.fs | 1 - src/Fantomas.Core/CodeFormatterImpl.fsi | 1 - 9 files changed, 40 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b319d4dcd0..6b78b2047e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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 diff --git a/docs/docs/end-users/GeneratingCode.fsx b/docs/docs/end-users/GeneratingCode.fsx index 5acf6a3484..4cec53a859 100644 --- a/docs/docs/end-users/GeneratingCode.fsx +++ b/docs/docs/end-users/GeneratingCode.fsx @@ -50,7 +50,7 @@ let implementationSyntaxTree = false, None, None, - Choice1Of2(IdentListNode([], Range.Zero)), + Choice1Of2(IdentListNode([ IdentifierOrDot.Ident(SingleTextNode("a", Range.Zero)) ], Range.Zero)), None, [], None, diff --git a/src/Fantomas.Core.Tests/FormatAstTests.fs b/src/Fantomas.Core.Tests/FormatAstTests.fs index be737082fd..00307214e9 100644 --- a/src/Fantomas.Core.Tests/FormatAstTests.fs +++ b/src/Fantomas.Core.Tests/FormatAstTests.fs @@ -15,7 +15,6 @@ let parseAndFormat sourceCode = let formattedCode = CodeFormatter.FormatASTAsync(ast, source = sourceCode) |> Async.RunSynchronously - |> fun formatResult -> formatResult.Code |> String.normalizeNewLine |> fun s -> s.TrimEnd('\n') diff --git a/src/Fantomas.Core.Tests/SynLongIdentTests.fs b/src/Fantomas.Core.Tests/SynLongIdentTests.fs index 7e0cb150fd..0c26795e12 100644 --- a/src/Fantomas.Core.Tests/SynLongIdentTests.fs +++ b/src/Fantomas.Core.Tests/SynLongIdentTests.fs @@ -404,5 +404,4 @@ let ``backticks can be added from AST only scenarios`` () = InsertFinalNewline = false } ) |> Async.RunSynchronously - |> fun result -> result.Code |> should equal "``new``" diff --git a/src/Fantomas.Core.Tests/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index 8dabb54a45..52ea5b0c1f 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -2,7 +2,6 @@ module Fantomas.Core.Tests.TestHelper open System open Fantomas.Core -open Fantomas.Core.SyntaxOak open NUnit.Framework open FsUnit @@ -42,13 +41,13 @@ let formatAST isFsiFile (source: string) config = let ast, _ = Fantomas.FCS.Parse.parseFile isFsiFile (FSharp.Compiler.Text.SourceText.ofString source) [] - let! formatted = CodeFormatter.FormatASTAsync(ast, config = config) - let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isFsiFile, formatted.Code) + 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.Code}" + failwithf $"The formatted result is not valid F# code or contains warnings\n%s{formattedCode}" - return formatted.Code.Replace("\r\n", "\n") + return formattedCode.Replace("\r\n", "\n") } |> Async.RunSynchronously diff --git a/src/Fantomas.Core/CodeFormatter.fs b/src/Fantomas.Core/CodeFormatter.fs index cdd70e714b..c808c51f13 100644 --- a/src/Fantomas.Core/CodeFormatter.fs +++ b/src/Fantomas.Core/CodeFormatter.fs @@ -12,21 +12,31 @@ type CodeFormatter = return results |> Array.map (fun (ast, DefineCombination(defines)) -> ast, defines) } - static member FormatASTAsync(ast: ParsedInput) : Async = - CodeFormatterImpl.formatAST ast None FormatConfig.Default None |> async.Return - - static member FormatASTAsync(ast: ParsedInput, config) : Async = - CodeFormatterImpl.formatAST ast None config None |> async.Return + static member FormatASTAsync(ast: ParsedInput) : Async = + async { + let result = CodeFormatterImpl.formatAST ast None FormatConfig.Default None + return result.Code + } - static member FormatASTAsync(ast: ParsedInput, source) : Async = - let sourceText = Some(CodeFormatterImpl.getSourceText source) + static member FormatASTAsync(ast: ParsedInput, config) : Async = + async { + let result = CodeFormatterImpl.formatAST ast None config None + return result.Code + } - CodeFormatterImpl.formatAST ast sourceText FormatConfig.Default None - |> async.Return + 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 = - let sourceText = Some(CodeFormatterImpl.getSourceText source) - CodeFormatterImpl.formatAST ast sourceText config None |> async.Return + 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 diff --git a/src/Fantomas.Core/CodeFormatter.fsi b/src/Fantomas.Core/CodeFormatter.fsi index ff8296487c..edf6cfb71e 100644 --- a/src/Fantomas.Core/CodeFormatter.fsi +++ b/src/Fantomas.Core/CodeFormatter.fsi @@ -10,13 +10,13 @@ type CodeFormatter = static member ParseAsync: isSignature: bool * source: string -> Async<(ParsedInput * string list) array> /// Format an abstract syntax tree - static member FormatASTAsync: ast: ParsedInput -> Async + static member FormatASTAsync: ast: ParsedInput -> Async /// Format an abstract syntax tree using a given config - static member FormatASTAsync: ast: ParsedInput * config: FormatConfig -> Async + static member FormatASTAsync: ast: ParsedInput * config: FormatConfig -> Async /// Format an abstract syntax tree with the original source for trivia processing - static member FormatASTAsync: ast: ParsedInput * source: string -> Async + static member FormatASTAsync: ast: ParsedInput * source: string -> Async /// /// Format a source string using an optional config. diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index c7ee04addb..f051473709 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -4,7 +4,6 @@ module internal Fantomas.Core.CodeFormatterImpl open FSharp.Compiler.Diagnostics open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open SyntaxOak open MultipleDefineCombinations let getSourceText (source: string) : ISourceText = source.TrimEnd() |> SourceText.ofString diff --git a/src/Fantomas.Core/CodeFormatterImpl.fsi b/src/Fantomas.Core/CodeFormatterImpl.fsi index 40a57bf7a1..83fbb373cb 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fsi +++ b/src/Fantomas.Core/CodeFormatterImpl.fsi @@ -3,7 +3,6 @@ module internal Fantomas.Core.CodeFormatterImpl open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open Fantomas.Core.SyntaxOak val getSourceText: source: string -> ISourceText