From c60836d413dd8dcf0d059aaff4d33b8cc5dea567 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 4 Apr 2024 23:00:50 +0200 Subject: [PATCH 1/3] Add treefmt, empty config for now --- .github/workflows/main.yml | 3 +++ default.nix | 13 ++++++++++++- npins/sources.json | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 420cc9c8..03dbf4fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,9 @@ jobs: - name: hlint run: nix-build -A checks.hlint + - name: treefmt + run: nix-build -A checks.treefmt + - name: build nixfmt run: nix-build if: success() || failure() diff --git a/default.nix b/default.nix index 530733d5..2082a0f8 100644 --- a/default.nix +++ b/default.nix @@ -39,6 +39,11 @@ let haskell.lib.dontHaddock (drv: lib.lazyDerivation { derivation = drv; }) ]; + + treefmtEval = (import sources.treefmt-nix).evalModule pkgs { + # Used to find the project root + projectRootFile = ".git/config"; + }; in build // { @@ -56,11 +61,17 @@ build shellcheck npins hlint + treefmtEval.config.build.wrapper ]; }; checks = { hlint = pkgs.build.haskell.hlint src; - stylish-haskell = pkgs.build.haskell.stylish-haskell ./.; + treefmt = treefmtEval.config.build.check ( + lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.gitTracked ./.; + } + ); }; } diff --git a/npins/sources.json b/npins/sources.json index a46565a5..a6c9d71a 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -17,6 +17,18 @@ "revision": "b6bbeda170469574789f9128c9aadc3a9e91e512", "url": "https://github.com/serokell/serokell.nix/archive/b6bbeda170469574789f9128c9aadc3a9e91e512.tar.gz", "hash": "1p6gsw00sbnpm01r6dg0m975xnr31xax1ciznl5rdpxarsrkibnn" + }, + "treefmt-nix": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "numtide", + "repo": "treefmt-nix" + }, + "branch": "main", + "revision": "49dc4a92b02b8e68798abd99184f228243b6e3ac", + "url": "https://github.com/numtide/treefmt-nix/archive/49dc4a92b02b8e68798abd99184f228243b6e3ac.tar.gz", + "hash": "0qlhb0xvcc3al19irclxk7vnppd9m6b5vi3nbjb9dylphs306x1p" } }, "version": 3 From c697c0a7a9406639acaf9a3325b896c777525261 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 4 Apr 2024 23:02:31 +0200 Subject: [PATCH 2/3] treefmt: Ensure all Nix files are formatted --- default.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/default.nix b/default.nix index 2082a0f8..ece72506 100644 --- a/default.nix +++ b/default.nix @@ -43,6 +43,12 @@ let treefmtEval = (import sources.treefmt-nix).evalModule pkgs { # Used to find the project root projectRootFile = ".git/config"; + + # This uses the version from Nixpkgs instead of the local one, + # which would require building the package to get a development shell + programs.nixfmt-rfc-style.enable = true; + # We don't want to format the files we use to test the formatter! + settings.formatter.nixfmt-rfc-style.excludes = [ "test/*" ]; }; in build From 27f80162729de92c77337ba96304ca91df9d41de Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 4 Apr 2024 23:02:50 +0200 Subject: [PATCH 3/3] treefmt: Ensure all Haskell files are formatted with fourmolu Haskell has various formatters and no standard one. While I originally wanted to use ormolu, we agreed to switch to fourmolu because we weren't super happy about some decisions in ormolu. --- default.nix | 3 + fourmolu.yaml | 51 ++ main/Main.hs | 180 +++--- main/System/IO/Atomic.hs | 36 +- main/System/IO/Utf8.hs | 27 +- src/Nixfmt.hs | 90 +-- src/Nixfmt/Lexer.hs | 205 ++++--- src/Nixfmt/Parser.hs | 457 +++++++++------ src/Nixfmt/Parser/Float.hs | 41 +- src/Nixfmt/Predoc.hs | 589 ++++++++++---------- src/Nixfmt/Pretty.hs | 1074 +++++++++++++++++++----------------- src/Nixfmt/Types.hs | 777 +++++++++++++------------- src/Nixfmt/Util.hs | 70 ++- 13 files changed, 1960 insertions(+), 1640 deletions(-) create mode 100644 fourmolu.yaml diff --git a/default.nix b/default.nix index ece72506..b71f464d 100644 --- a/default.nix +++ b/default.nix @@ -49,6 +49,9 @@ let programs.nixfmt-rfc-style.enable = true; # We don't want to format the files we use to test the formatter! settings.formatter.nixfmt-rfc-style.excludes = [ "test/*" ]; + + # Haskell formatter + programs.fourmolu.enable = true; }; in build diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 00000000..f082a8af --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,51 @@ +# Number of spaces per indentation step +indentation: 2 + +# Max line length for automatic line breaking +column-limit: none + +# Styling of arrows in type signatures (choices: trailing, leading, or leading-args) +function-arrows: trailing + +# How to place commas in multi-line lists, records, etc. (choices: leading or trailing) +comma-style: trailing + +# Styling of import/export lists (choices: leading, trailing, or diff-friendly) +import-export-style: diff-friendly + +# Whether to full-indent or half-indent 'where' bindings past the preceding body +indent-wheres: true + +# Whether to leave a space before an opening record brace +record-brace-space: false + +# Number of spaces between top-level declarations +newlines-between-decls: 1 + +# How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact) +haddock-style: single-line + +# How to print module docstring +haddock-style-module: null + +# Styling of let blocks (choices: auto, inline, newline, or mixed) +let-style: inline + +# How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space) +in-style: no-space + +# Whether to put parentheses around a single constraint (choices: auto, always, or never) +single-constraint-parens: always + +# Output Unicode syntax (choices: detect, always, or never) +unicode: never + +# Give the programmer more choice on where to insert blank lines +respectful: true + +# Fixity information for operators +fixities: [] + +# Module reexports Fourmolu should know about +reexports: [] + diff --git a/main/Main.hs b/main/Main.hs index c793854d..97424bdd 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -1,76 +1,87 @@ -{-# LANGUAGE DeriveDataTypeable, NamedFieldPuns, MultiWayIf #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} module Main where import Control.Concurrent (Chan, forkIO, newChan, readChan, writeChan) import Data.Either (lefts) -import Data.Text (Text) import Data.List (isSuffixOf) +import Data.Text (Text) +import qualified Data.Text.IO as TextIO (getContents, hPutStr, putStr) import Data.Version (showVersion) import GHC.IO.Encoding (utf8) +import qualified Nixfmt import Paths_nixfmt (version) -import System.Console.CmdArgs - (Data, Typeable, args, cmdArgs, help, summary, typ, (&=)) -import System.Exit (ExitCode(..), exitFailure, exitSuccess) +import System.Console.CmdArgs ( + Data, + Typeable, + args, + cmdArgs, + help, + summary, + typ, + (&=), + ) +import System.Directory (doesDirectoryExist, listDirectory) +import System.Exit (ExitCode (..), exitFailure, exitSuccess) import System.FilePath (()) import System.IO (hPutStrLn, hSetEncoding, stderr) -import System.Posix.Process (exitImmediately) -import System.Posix.Signals (Handler(..), installHandler, keyboardSignal) -import System.Directory (listDirectory, doesDirectoryExist) - -import qualified Data.Text.IO as TextIO (getContents, hPutStr, putStr) - -import qualified Nixfmt import System.IO.Atomic (withOutputFile) import System.IO.Utf8 (readFileUtf8, withUtf8StdHandles) +import System.Posix.Process (exitImmediately) +import System.Posix.Signals (Handler (..), installHandler, keyboardSignal) type Result = Either String () + type Width = Int data Nixfmt = Nixfmt - { files :: [FilePath] - , width :: Width - , check :: Bool - , quiet :: Bool - , verify :: Bool - } deriving (Show, Data, Typeable) + { files :: [FilePath], + width :: Width, + check :: Bool, + quiet :: Bool, + verify :: Bool + } + deriving (Show, Data, Typeable) options :: Nixfmt options = let defaultWidth = 100 addDefaultHint value message = message ++ "\n[default: " ++ show value ++ "]" - in Nixfmt - { files = [] &= args &= typ "FILES" - , width = - defaultWidth &= - help (addDefaultHint defaultWidth "Maximum width in characters") - , check = False &= help "Check whether files are formatted without modifying them" - , quiet = False &= help "Do not report errors" - , verify = - False &= - help + in Nixfmt + { files = [] &= args &= typ "FILES", + width = + defaultWidth + &= help (addDefaultHint defaultWidth "Maximum width in characters"), + check = False &= help "Check whether files are formatted without modifying them", + quiet = False &= help "Do not report errors", + verify = + False + &= help "Apply sanity checks on the output after formatting" - } &= - summary ("nixfmt v" ++ showVersion version) &= - help "Format Nix source code" + } + &= summary ("nixfmt v" ++ showVersion version) + &= help "Format Nix source code" data Target = Target - { tDoRead :: IO Text - , tPath :: FilePath - -- The bool is true when the formatted file differs from the input - , tDoWrite :: Bool -> Text -> IO () - } + { tDoRead :: IO Text, + tPath :: FilePath, + -- The bool is true when the formatted file differs from the input + tDoWrite :: Bool -> Text -> IO () + } -- | Recursively collect nix files in a directory collectNixFiles :: FilePath -> IO [FilePath] collectNixFiles path = do dir <- doesDirectoryExist path - if | dir -> do - files <- listDirectory path - concat <$> mapM collectNixFiles ((path ) <$> files) - | ".nix" `isSuffixOf` path -> pure [path] - | otherwise -> pure [] + if + | dir -> do + files <- listDirectory path + concat <$> mapM collectNixFiles ((path ) <$> files) + | ".nix" `isSuffixOf` path -> pure [path] + | otherwise -> pure [] -- | Recursively collect nix files in a list of directories collectAllNixFiles :: [FilePath] -> IO [FilePath] @@ -78,20 +89,20 @@ collectAllNixFiles paths = concat <$> mapM collectNixFiles paths formatTarget :: Formatter -> Target -> IO Result formatTarget format Target{tDoRead, tPath, tDoWrite} = do - contents <- tDoRead - let formatResult = format tPath contents - mapM (\formatted -> tDoWrite (formatted /= contents) formatted) formatResult + contents <- tDoRead + let formatResult = format tPath contents + mapM (\formatted -> tDoWrite (formatted /= contents) formatted) formatResult -- | Return an error if target could not be parsed or was not formatted -- correctly. checkTarget :: Formatter -> Target -> IO Result checkTarget format Target{tDoRead, tPath} = do - contents <- tDoRead - return $ case format tPath contents of - Left err -> Left err - Right formatted - | formatted == contents -> Right () - | otherwise -> Left $ tPath ++ ": not formatted" + contents <- tDoRead + return $ case format tPath contents of + Left err -> Left err + Right formatted + | formatted == contents -> Right () + | otherwise -> Left $ tPath ++ ": not formatted" stdioTarget :: Target stdioTarget = Target TextIO.getContents "" (const TextIO.putStr) @@ -109,26 +120,26 @@ checkFileTarget :: FilePath -> Target checkFileTarget path = Target (readFileUtf8 path) path (const $ const $ pure ()) toTargets :: Nixfmt -> IO [Target] -toTargets Nixfmt{ files = [] } = pure [stdioTarget] -toTargets Nixfmt{ files = ["-"] } = pure [stdioTarget] -toTargets Nixfmt{ check = False, files = paths } = map fileTarget <$> collectAllNixFiles paths -toTargets Nixfmt{ check = True, files = paths } = map checkFileTarget <$> collectAllNixFiles paths +toTargets Nixfmt{files = []} = pure [stdioTarget] +toTargets Nixfmt{files = ["-"]} = pure [stdioTarget] +toTargets Nixfmt{check = False, files = paths} = map fileTarget <$> collectAllNixFiles paths +toTargets Nixfmt{check = True, files = paths} = map checkFileTarget <$> collectAllNixFiles paths type Formatter = FilePath -> Text -> Either String Text toFormatter :: Nixfmt -> Formatter -toFormatter Nixfmt{ width, verify = True } = Nixfmt.formatVerify width -toFormatter Nixfmt{ width, verify = False } = Nixfmt.format width +toFormatter Nixfmt{width, verify = True} = Nixfmt.formatVerify width +toFormatter Nixfmt{width, verify = False} = Nixfmt.format width type Operation = Formatter -> Target -> IO Result toOperation :: Nixfmt -> Operation -toOperation Nixfmt{ check = True } = checkTarget -toOperation Nixfmt{ } = formatTarget +toOperation Nixfmt{check = True} = checkTarget +toOperation Nixfmt{} = formatTarget toWriteError :: Nixfmt -> String -> IO () -toWriteError Nixfmt{ quiet = False } = hPutStrLn stderr -toWriteError Nixfmt{ quiet = True } = const $ return () +toWriteError Nixfmt{quiet = False} = hPutStrLn stderr +toWriteError Nixfmt{quiet = True} = const $ return () toJobs :: Nixfmt -> IO [IO Result] toJobs opts = map (toOperation opts $ toFormatter opts) <$> toTargets opts @@ -141,36 +152,39 @@ doParallel = sequence errorWriter :: (String -> IO ()) -> Chan (Maybe String) -> Chan () -> IO () errorWriter doWrite chan done = do - item <- readChan chan - case item of - Nothing -> return () - Just msg -> doWrite msg >> errorWriter doWrite chan done - writeChan done () + item <- readChan chan + case item of + Nothing -> return () + Just msg -> doWrite msg >> errorWriter doWrite chan done + writeChan done () writeErrorBundle :: Chan (Maybe String) -> Result -> IO Result writeErrorBundle chan result = do - case result of - Right () -> return () - Left err -> writeChan chan $ Just err - return result + case result of + Right () -> return () + Left err -> writeChan chan $ Just err + return result -- | Run a list of jobs and write errors to stderr without interleaving them. runJobs :: (String -> IO ()) -> [IO Result] -> IO [Result] runJobs writeError jobs = do - errChan <- newChan - doneChan <- newChan - _ <- forkIO $ errorWriter writeError errChan doneChan - results <- doParallel $ map (>>= writeErrorBundle errChan) jobs - writeChan errChan Nothing - _ <- readChan doneChan - return results + errChan <- newChan + doneChan <- newChan + _ <- forkIO $ errorWriter writeError errChan doneChan + results <- doParallel $ map (>>= writeErrorBundle errChan) jobs + writeChan errChan Nothing + _ <- readChan doneChan + return results main :: IO () main = withUtf8StdHandles $ do - _ <- installHandler keyboardSignal - (Catch (exitImmediately $ ExitFailure 2)) Nothing - opts <- cmdArgs options - results <- runJobs (toWriteError opts) =<< toJobs opts - case lefts results of - [] -> exitSuccess - _ -> exitFailure + _ <- + installHandler + keyboardSignal + (Catch (exitImmediately $ ExitFailure 2)) + Nothing + opts <- cmdArgs options + results <- runJobs (toWriteError opts) =<< toJobs opts + case lefts results of + [] -> exitSuccess + _ -> exitFailure diff --git a/main/System/IO/Atomic.hs b/main/System/IO/Atomic.hs index 91e36097..65a08218 100644 --- a/main/System/IO/Atomic.hs +++ b/main/System/IO/Atomic.hs @@ -5,9 +5,10 @@ -- -- This module is a crossover of the `atomic-write` and `safeio` libraries -- with the fs-related behaviour of the first and interface of the second. -module System.IO.Atomic - ( withOutputFile - ) where +module System.IO.Atomic ( + withOutputFile, +) +where import Control.Exception.Safe (mask, onException) import Control.Monad (when) @@ -16,7 +17,6 @@ import System.FilePath (takeDirectory, takeFileName) import System.IO (Handle, hClose, openTempFileWithDefaultPermissions) import System.Posix.Files (fileMode, getFileStatus, setFileMode) - -- | Like @withFile@ but replaces the contents atomically. -- -- This function allocates a temporary file and provides its handle to the @@ -28,12 +28,14 @@ import System.Posix.Files (fileMode, getFileStatus, setFileMode) -- the attributes is a challenge. This function tries its best, but currently -- it is Unix-specific and there is definitely room for improvement even on Unix. withOutputFile :: - FilePath -- ^ Final file path - -> (Handle -> IO a) -- ^ IO action that writes to the file - -> IO a + -- | Final file path + FilePath -> + -- | IO action that writes to the file + (Handle -> IO a) -> + IO a withOutputFile path act = transact begin commit rollback $ \(tpath, th) -> do - copyAttributes (tpath, th) - act th + copyAttributes (tpath, th) + act th where tmpDir = takeDirectory path tmpTemplate = "." <> takeFileName path <> ".atomic" @@ -56,8 +58,6 @@ withOutputFile path act = transact begin commit rollback $ \(tpath, th) -> do rollback :: (FilePath, Handle) -> IO () rollback (tpath, th) = hClose th *> removeFile tpath - - --- -- Helpers -- @@ -72,11 +72,15 @@ withOutputFile path act = transact begin commit rollback $ \(tpath, th) -> do -- here, if the action completes but commit fails, we will still run rollback, -- so there exists an execution in which both finalisation actions are run. transact :: - IO a -- ^ computation to run first (\"begin transaction\") - -> (a -> IO b) -- ^ computation to run on success (\"commit transaction\") - -> (a -> IO c) -- ^ computation to run on failure (\"rollback transaction\") - -> (a -> IO d) -- ^ computation to run in-between - -> IO d -- returns the value from the in-between computation + -- | computation to run first (\"begin transaction\") + IO a -> + -- | computation to run on success (\"commit transaction\") + (a -> IO b) -> + -- | computation to run on failure (\"rollback transaction\") + (a -> IO c) -> + -- | computation to run in-between + (a -> IO d) -> + IO d -- returns the value from the in-between computation transact begin commit rollback act = mask $ \restore -> do a <- begin diff --git a/main/System/IO/Utf8.hs b/main/System/IO/Utf8.hs index 382f7ef4..0ce3e4cd 100644 --- a/main/System/IO/Utf8.hs +++ b/main/System/IO/Utf8.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE ViewPatterns #-} -- | Helpers for implementing tools that process UTF-8 encoded data. @@ -15,22 +15,20 @@ -- encoding is not UTF-8, using UTF-8 might actually be unsafe. -- -- Functions in this module help deal with all these issues. -module System.IO.Utf8 - ( withUtf8StdHandles - - , openFileUtf8 - , readFileUtf8 - ) where +module System.IO.Utf8 ( + withUtf8StdHandles, + openFileUtf8, + readFileUtf8, +) +where import Control.Exception (bracket) import Data.Text (Text) +import qualified Data.Text.IO as T import GHC.IO.Encoding (mkTextEncoding, textEncodingName, utf8) import System.IO (stderr, stdin, stdout) - -import qualified Data.Text.IO as T import qualified System.IO as IO - type EncRestoreAction = IO.Handle -> IO () -- | Sets the best available UTF-8-compatible encoding for the handle. @@ -41,7 +39,8 @@ type EncRestoreAction = IO.Handle -> IO () -- Otherwise, keeps its current encoding, but augments it to transliterate -- unsupported characters. hSetBestUtf8Enc :: IO.Handle -> IO EncRestoreAction -hSetBestUtf8Enc h = IO.hGetEncoding h >>= \case +hSetBestUtf8Enc h = + IO.hGetEncoding h >>= \case Nothing -> pure (\_ -> pure ()) Just enc -> do isTerm <- IO.hIsTerminalDevice h @@ -59,15 +58,13 @@ hSetBestUtf8Enc h = IO.hGetEncoding h >>= \case -- After the action finishes, restores the original encodings. withUtf8StdHandles :: IO a -> IO a withUtf8StdHandles action = - withConfiguredHandle stdin $ + withConfiguredHandle stdin $ withConfiguredHandle stdout $ - withConfiguredHandle stderr action + withConfiguredHandle stderr action where withConfiguredHandle :: IO.Handle -> IO a -> IO a withConfiguredHandle h = bracket (hSetBestUtf8Enc h) ($ h) . const - - -- | Like @openFile@, but sets the file encoding to UTF-8, regardless -- of the current locale. openFileUtf8 :: IO.FilePath -> IO.IOMode -> IO IO.Handle diff --git a/src/Nixfmt.hs b/src/Nixfmt.hs index 9454d784..e831eac7 100644 --- a/src/Nixfmt.hs +++ b/src/Nixfmt.hs @@ -1,21 +1,21 @@ -module Nixfmt - ( errorBundlePretty - , ParseErrorBundle - , Width - , format - , formatVerify - ) where +module Nixfmt ( + errorBundlePretty, + ParseErrorBundle, + Width, + format, + formatVerify, +) +where -import Data.Either (fromRight) import Data.Bifunctor (bimap, first) +import Data.Either (fromRight) import Data.Text (Text, unpack) -import qualified Text.Megaparsec as Megaparsec (parse) -import Text.Megaparsec.Error (errorBundlePretty) - import Nixfmt.Parser (file) import Nixfmt.Predoc (layout) import Nixfmt.Pretty () -import Nixfmt.Types (ParseErrorBundle, Whole(..), Expression, walkSubprograms) +import Nixfmt.Types (Expression, ParseErrorBundle, Whole (..), walkSubprograms) +import qualified Text.Megaparsec as Megaparsec (parse) +import Text.Megaparsec.Error (errorBundlePretty) -- import Debug.Trace (traceShow, traceShowId) @@ -25,8 +25,8 @@ type Width = Int -- failure in @filename@ or a formatted version of @source@ with a maximum width -- of @w@ columns where possible. format :: Width -> FilePath -> Text -> Either String Text -format width filename - = bimap errorBundlePretty (layout width) +format width filename = + bimap errorBundlePretty (layout width) . Megaparsec.parse file filename -- Same functionality as `format`, but add sanity checks to guarantee the following properties of the formatter: @@ -37,35 +37,39 @@ format width filename -- the issue on an automatically minimized example based on the input. formatVerify :: Width -> FilePath -> Text -> Either String Text formatVerify width path unformatted = do - unformattedParsed@(Whole unformattedParsed' _) <- parse unformatted - let formattedOnce = layout width unformattedParsed - formattedOnceParsed <- first (\x -> pleaseReport "Fails to parse after formatting.\n" <> x <> "\n\nAfter Formatting:\n" <> unpack formattedOnce) (parse formattedOnce) - let formattedTwice = layout width formattedOnceParsed - if formattedOnceParsed /= unformattedParsed - then Left $ - let - minimized = minimize unformattedParsed' (\e -> parse (layout width e) == Right (Whole e [])) - in - pleaseReport "Parses differently after formatting." - <> "\n\nBefore formatting:\n" <> show minimized - <> "\n\nAfter formatting:\n" <> show (fromRight (error "TODO") $ parse (layout width minimized)) - else if formattedOnce /= formattedTwice - then Left $ - let - minimized = minimize unformattedParsed' - (\e -> layout width e == layout width (fromRight (error "TODO") $ parse $ layout width e)) - in - pleaseReport "Nixfmt is not idempotent." - <> "\n\nAfter one formatting:\n" <> unpack (layout width minimized) - <> "\n\nAfter two:\n" <> unpack (layout width (fromRight (error "TODO") $ parse $ layout width minimized)) - else Right formattedOnce - where - parse = first errorBundlePretty . Megaparsec.parse file path - pleaseReport x = path <> ": " <> x <> " This is a bug in nixfmt. Please report it at https://github.com/NixOS/nixfmt" - + unformattedParsed@(Whole unformattedParsed' _) <- parse unformatted + let formattedOnce = layout width unformattedParsed + formattedOnceParsed <- first (\x -> pleaseReport "Fails to parse after formatting.\n" <> x <> "\n\nAfter Formatting:\n" <> unpack formattedOnce) (parse formattedOnce) + let formattedTwice = layout width formattedOnceParsed + if formattedOnceParsed /= unformattedParsed + then + Left $ + let minimized = minimize unformattedParsed' (\e -> parse (layout width e) == Right (Whole e [])) + in pleaseReport "Parses differently after formatting." + <> "\n\nBefore formatting:\n" + <> show minimized + <> "\n\nAfter formatting:\n" + <> show (fromRight (error "TODO") $ parse (layout width minimized)) + else + if formattedOnce /= formattedTwice + then + Left $ + let minimized = + minimize + unformattedParsed' + (\e -> layout width e == layout width (fromRight (error "TODO") $ parse $ layout width e)) + in pleaseReport "Nixfmt is not idempotent." + <> "\n\nAfter one formatting:\n" + <> unpack (layout width minimized) + <> "\n\nAfter two:\n" + <> unpack (layout width (fromRight (error "TODO") $ parse $ layout width minimized)) + else Right formattedOnce + where + parse = first errorBundlePretty . Megaparsec.parse file path + pleaseReport x = path <> ": " <> x <> " This is a bug in nixfmt. Please report it at https://github.com/NixOS/nixfmt" minimize :: Expression -> (Expression -> Bool) -> Expression minimize expr test = - case concatMap (\e -> ([minimize e test | not (test e)])) $ walkSubprograms expr of - result:_ -> result - [] -> expr + case concatMap (\e -> ([minimize e test | not (test e)])) $ walkSubprograms expr of + result : _ -> result + [] -> expr diff --git a/src/Nixfmt/Lexer.hs b/src/Nixfmt/Lexer.hs index 4a0b018d..7a97a1a7 100644 --- a/src/Nixfmt/Lexer.hs +++ b/src/Nixfmt/Lexer.hs @@ -1,4 +1,7 @@ -{-# LANGUAGE BlockArguments, FlexibleContexts, LambdaCase, OverloadedStrings #-} +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Nixfmt.Lexer (lexeme, pushTrivia, takeTrivia, whole) where @@ -6,26 +9,57 @@ import Control.Monad.State (MonadState, evalStateT, get, modify, put) import Data.Char (isSpace) import Data.List (dropWhileEnd) import Data.Maybe (fromMaybe) -import Data.Text as Text - (Text, length, lines, null, pack, replace, replicate, strip, stripEnd, - stripPrefix, stripStart, takeWhile, unwords, isPrefixOf) +import Data.Text as Text ( + Text, + isPrefixOf, + length, + lines, + null, + pack, + replace, + replicate, + strip, + stripEnd, + stripPrefix, + stripStart, + takeWhile, + unwords, + ) import Data.Void (Void) -import Text.Megaparsec - (Parsec, SourcePos(..), Pos, anySingle, chunk, getSourcePos, hidden, many, - manyTill, some, try, unPos, (<|>), notFollowedBy) -import Text.Megaparsec.Char (eol, char) - -import Nixfmt.Types - (Ann(..), Whole(..), Parser, TrailingComment(..), Trivia, Trivium(..)) -import Nixfmt.Util (manyP, isSpaces) +import Nixfmt.Types ( + Ann (..), + Parser, + TrailingComment (..), + Trivia, + Trivium (..), + Whole (..), + ) +import Nixfmt.Util (isSpaces, manyP) +import Text.Megaparsec ( + Parsec, + Pos, + SourcePos (..), + anySingle, + chunk, + getSourcePos, + hidden, + many, + manyTill, + notFollowedBy, + some, + try, + unPos, + (<|>), + ) +import Text.Megaparsec.Char (char, eol) data ParseTrivium - = PTNewlines Int - -- Track the column where the comment starts - | PTLineComment Text Pos - -- Track whether it is a doc comment - | PTBlockComment Bool [Text] - deriving (Show) + = PTNewlines Int + | -- Track the column where the comment starts + PTLineComment Text Pos + | -- Track whether it is a doc comment + PTBlockComment Bool [Text] + deriving (Show) preLexeme :: Parser a -> Parser a preLexeme p = p <* manyP (\x -> isSpace x && x /= '\n' && x /= '\r') @@ -35,24 +69,23 @@ newlines = PTNewlines . Prelude.length <$> some (preLexeme eol) lineComment :: Parser ParseTrivium lineComment = preLexeme $ do - SourcePos{sourceColumn = col} <- getSourcePos - _ <- chunk "#" - text <- manyP (\x -> x /= '\n' && x /= '\r') - return (PTLineComment text col) + SourcePos{sourceColumn = col} <- getSourcePos + _ <- chunk "#" + text <- manyP (\x -> x /= '\n' && x /= '\r') + return (PTLineComment text col) blockComment :: Parser ParseTrivium blockComment = try $ preLexeme $ do - SourcePos{sourceColumn = pos} <- getSourcePos - -- Positions start counting at 1, which we don't want here - let pos' = unPos pos - 1 - _ <- chunk "/*" - -- Try to parse /** before /*, but don't parse /**/ (i.e. the empty comment) - isDoc <- try ((True <$ char '*') <* notFollowedBy (char '/')) <|> pure False - - chars <- manyTill anySingle $ chunk "*/" - return $ PTBlockComment isDoc $ dropWhile Text.null $ fixIndent pos' $ removeStars pos' $ splitLines $ pack chars - - where + SourcePos{sourceColumn = pos} <- getSourcePos + -- Positions start counting at 1, which we don't want here + let pos' = unPos pos - 1 + _ <- chunk "/*" + -- Try to parse /** before /*, but don't parse /**/ (i.e. the empty comment) + isDoc <- try ((True <$ char '*') <* notFollowedBy (char '/')) <|> pure False + + chars <- manyTill anySingle $ chunk "*/" + return $ PTBlockComment isDoc $ dropWhile Text.null $ fixIndent pos' $ removeStars pos' $ splitLines $ pack chars + where -- Normalize line ends and stuff splitLines :: Text -> [Text] splitLines = dropWhileEnd Text.null . map Text.stripEnd . Text.lines . replace "\r\n" "\n" @@ -62,65 +95,69 @@ blockComment = try $ preLexeme $ do removeStars :: Int -> [Text] -> [Text] removeStars _ [] = [] removeStars pos (h : t) = - -- Replace the * with whitespace. Only do so when all lines correctly match. - -- The * must be aligned with the opening /* - h : (fromMaybe t . traverse (fmap (newStart <>) . stripPrefix start) $ t) - where - start = Text.replicate pos " " <> " *" - newStart = Text.replicate pos " " + -- Replace the * with whitespace. Only do so when all lines correctly match. + -- The * must be aligned with the opening /* + h : (fromMaybe t . traverse (fmap (newStart <>) . stripPrefix start) $ t) + where + start = Text.replicate pos " " <> " *" + newStart = Text.replicate pos " " -- Strip the indented prefix of all lines -- If the first line is empty, we set the minimum indentation to +2. -- However, if there is a first line and it is aligned with the others, use +3 instead. fixIndent :: Int -> [Text] -> [Text] - fixIndent _ [] = [] - fixIndent pos (h : t) - = strip h : map (stripIndentation $ commonIndentationLength offset $ filter (not . isSpaces) t) t - where - offset = if " " `isPrefixOf` h then pos + 3 else pos + 2 + fixIndent _ [] = [] + fixIndent pos (h : t) = + strip h : map (stripIndentation $ commonIndentationLength offset $ filter (not . isSpaces) t) t + where + offset = if " " `isPrefixOf` h then pos + 3 else pos + 2 stripIndentation :: Int -> Text -> Text stripIndentation n t = fromMaybe (stripStart t) $ stripPrefix (Text.replicate n " ") t commonIndentationLength :: Int -> [Text] -> Int - commonIndentationLength = foldr (min . Text.length . Text.takeWhile (==' ')) + commonIndentationLength = foldr (min . Text.length . Text.takeWhile (== ' ')) -- This should be called with zero or one elements, as per `span isTrailing` convertTrailing :: [ParseTrivium] -> Maybe TrailingComment convertTrailing = toMaybe . join . map toText - where toText (PTLineComment c _) = strip c - toText (PTBlockComment False [c]) = strip c - toText _ = "" - join = Text.unwords . filter (/="") - toMaybe "" = Nothing - toMaybe c = Just $ TrailingComment c + where + toText (PTLineComment c _) = strip c + toText (PTBlockComment False [c]) = strip c + toText _ = "" + join = Text.unwords . filter (/= "") + toMaybe "" = Nothing + toMaybe c = Just $ TrailingComment c convertLeading :: [ParseTrivium] -> Trivia -convertLeading = concatMap (\case - PTNewlines 1 -> [] - PTNewlines _ -> [EmptyLine] - PTLineComment c _ -> [LineComment c] - PTBlockComment _ [] -> [] - PTBlockComment False [c] -> [LineComment $ " " <> strip c] - PTBlockComment isDoc cs -> [BlockComment isDoc cs]) +convertLeading = + concatMap + ( \case + PTNewlines 1 -> [] + PTNewlines _ -> [EmptyLine] + PTLineComment c _ -> [LineComment c] + PTBlockComment _ [] -> [] + PTBlockComment False [c] -> [LineComment $ " " <> strip c] + PTBlockComment isDoc cs -> [BlockComment isDoc cs] + ) isTrailing :: ParseTrivium -> Bool -isTrailing (PTLineComment _ _) = True -isTrailing (PTBlockComment False []) = True +isTrailing (PTLineComment _ _) = True +isTrailing (PTBlockComment False []) = True isTrailing (PTBlockComment False [_]) = True -isTrailing _ = False +isTrailing _ = False convertTrivia :: [ParseTrivium] -> Pos -> (Maybe TrailingComment, Trivia) convertTrivia pts nextCol = - let (trailing, leading) = span isTrailing pts - in case (trailing, leading) of - -- Special case: if the trailing comment visually forms a block with the start of the following line, - -- then treat it like part of those comments instead of a distinct trailing comment. - -- This happens especially often after `{` or `[` tokens, where the comment of the first item - -- starts on the same line ase the opening token. - ([PTLineComment _ pos], (PTNewlines 1):(PTLineComment _ pos'):_) | pos == pos' -> (Nothing, convertLeading pts) - ([PTLineComment _ pos], [PTNewlines 1]) | pos == nextCol -> (Nothing, convertLeading pts) - _ -> (convertTrailing trailing, convertLeading leading) + let (trailing, leading) = span isTrailing pts + in case (trailing, leading) of + -- Special case: if the trailing comment visually forms a block with the start of the following line, + -- then treat it like part of those comments instead of a distinct trailing comment. + -- This happens especially often after `{` or `[` tokens, where the comment of the first item + -- starts on the same line ase the opening token. + ([PTLineComment _ pos], (PTNewlines 1) : (PTLineComment _ pos') : _) | pos == pos' -> (Nothing, convertLeading pts) + ([PTLineComment _ pos], [PTNewlines 1]) | pos == nextCol -> (Nothing, convertLeading pts) + _ -> (convertTrailing trailing, convertLeading leading) trivia :: Parser [ParseTrivium] trivia = many $ hidden $ lineComment <|> blockComment <|> newlines @@ -128,28 +165,28 @@ trivia = many $ hidden $ lineComment <|> blockComment <|> newlines -- The following primitives to interact with the state monad that stores trivia -- are designed to prevent trivia from being dropped or duplicated by accident. -takeTrivia :: MonadState Trivia m => m Trivia +takeTrivia :: (MonadState Trivia m) => m Trivia takeTrivia = get <* put [] -pushTrivia :: MonadState Trivia m => Trivia -> m () -pushTrivia t = modify (<>t) +pushTrivia :: (MonadState Trivia m) => Trivia -> m () +pushTrivia t = modify (<> t) lexeme :: Parser a -> Parser (Ann a) lexeme p = do - lastLeading <- takeTrivia - token <- preLexeme p - parsedTrivia <- trivia - -- This is the position of the next lexeme after the currently parsed one - SourcePos{sourceColumn = col} <- getSourcePos - let (trailing, nextLeading) = convertTrivia parsedTrivia col - pushTrivia nextLeading - return $ Ann lastLeading token trailing + lastLeading <- takeTrivia + token <- preLexeme p + parsedTrivia <- trivia + -- This is the position of the next lexeme after the currently parsed one + SourcePos{sourceColumn = col} <- getSourcePos + let (trailing, nextLeading) = convertTrivia parsedTrivia col + pushTrivia nextLeading + return $ Ann lastLeading token trailing -- | Tokens normally have only leading trivia and one trailing comment on the same -- line. A whole x also parses and stores final trivia after the x. A whole also -- does not interact with the trivia state of its surroundings. whole :: Parser a -> Parsec Void Text (Whole a) whole pa = flip evalStateT [] do - preLexeme $ pure () - pushTrivia . convertLeading =<< trivia - Whole <$> pa <*> takeTrivia + preLexeme $ pure () + pushTrivia . convertLeading =<< trivia + Whole <$> pa <*> takeTrivia diff --git a/src/Nixfmt/Parser.hs b/src/Nixfmt/Parser.hs index 3ba27fbf..63a47918 100644 --- a/src/Nixfmt/Parser.hs +++ b/src/Nixfmt/Parser.hs @@ -1,13 +1,14 @@ -{-# LANGUAGE FlexibleInstances, OverloadedStrings #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} module Nixfmt.Parser where -import Prelude hiding (String) - import Control.Monad (guard, liftM2) import Control.Monad.Combinators (sepBy) -import qualified Control.Monad.Combinators.Expr as MPExpr - (Operator(..), makeExprParser) +import qualified Control.Monad.Combinators.Expr as MPExpr ( + Operator (..), + makeExprParser, + ) import Control.Monad.Trans.Class (lift) import Data.Char (isAlpha) import Data.Foldable (toList) @@ -16,22 +17,64 @@ import Data.Maybe (fromMaybe, mapMaybe, maybeToList) import Data.Text (Text) import qualified Data.Text as Text import Data.Void (Void) -import Text.Megaparsec - (Parsec, anySingle, chunk, empty, eof, label, lookAhead, many, notFollowedBy, - oneOf, optional, satisfy, some, try, (<|>)) -import Text.Megaparsec.Char (char) -import qualified Text.Megaparsec.Char.Lexer as L (decimal) - import Nixfmt.Lexer (lexeme, pushTrivia, takeTrivia, whole) import Nixfmt.Parser.Float (floatParse) -import Nixfmt.Types - (Ann(..), Binder(..), Expression(..), File, Fixity(..), Item(..), Items(..), Leaf, - Operator(..), ParamAttr(..), Parameter(..), Parser, Path, Selector(..), - SimpleSelector(..), StringPart(..), Term(..), Token(..), Trivium(..), Whole(..), - operators, tokenText) -import Nixfmt.Util - (commonIndentation, identChar, isSpaces, manyP, manyText, pathChar, - schemeChar, someP, someText, uriChar) +import Nixfmt.Types ( + Ann (..), + Binder (..), + Expression (..), + File, + Fixity (..), + Item (..), + Items (..), + Leaf, + Operator (..), + ParamAttr (..), + Parameter (..), + Parser, + Path, + Selector (..), + SimpleSelector (..), + StringPart (..), + Term (..), + Token (..), + Trivium (..), + Whole (..), + operators, + tokenText, + ) +import Nixfmt.Util ( + commonIndentation, + identChar, + isSpaces, + manyP, + manyText, + pathChar, + schemeChar, + someP, + someText, + uriChar, + ) +import Text.Megaparsec ( + Parsec, + anySingle, + chunk, + empty, + eof, + label, + lookAhead, + many, + notFollowedBy, + oneOf, + optional, + satisfy, + some, + try, + (<|>), + ) +import Text.Megaparsec.Char (char) +import qualified Text.Megaparsec.Char.Lexer as L (decimal) +import Prelude hiding (String) -- HELPER FUNCTIONS @@ -47,17 +90,23 @@ symbol = lexeme . rawSymbol reservedNames :: [Text] reservedNames = - [ "let", "in" - , "if", "then", "else" - , "assert" - , "with" - , "rec" - , "inherit" - ] + [ "let", + "in", + "if", + "then", + "else", + "assert", + "with", + "rec", + "inherit" + ] reserved :: Token -> Parser (Ann Token) -reserved t = try $ lexeme $ rawSymbol t - <* lookAhead (satisfy (\x -> not $ identChar x || pathChar x)) +reserved t = + try $ + lexeme $ + rawSymbol t + <* lookAhead (satisfy (\x -> not $ identChar x || pathChar x)) -- VALUES @@ -69,24 +118,28 @@ float = ann Float floatParse identifier :: Parser (Ann Token) identifier = ann Identifier $ do - ident <- Text.cons <$> satisfy (\x -> isAlpha x || x == '_') - <*> manyP identChar - guard $ ident `notElem` reservedNames - return ident + ident <- + Text.cons + <$> satisfy (\x -> isAlpha x || x == '_') + <*> manyP identChar + guard $ ident `notElem` reservedNames + return ident slash :: Parser Text slash = chunk "/" <* notFollowedBy (char '/') -instance Semigroup a => Semigroup (Parser a) where +instance (Semigroup a) => Semigroup (Parser a) where fx <> fy = do x <- fx y <- fy pure $ x <> y envPath :: Parser (Ann Token) -envPath = ann EnvPath $ char '<' *> - someP pathChar <> manyText (slash <> someP pathChar) - <* char '>' +envPath = + ann EnvPath $ + char '<' + *> someP pathChar <> manyText (slash <> someP pathChar) + <* char '>' pathText :: Parser StringPart pathText = TextPart <$> someP pathChar @@ -95,18 +148,24 @@ pathTraversal :: Parser [StringPart] pathTraversal = liftM2 (:) (TextPart <$> slash) (some (pathText <|> interpolation)) path :: Parser Path -path = try $ lexeme $ fmap normalizeLine $ - (maybeToList <$> optional pathText) <> (concat <$> some pathTraversal) +path = + try $ + lexeme $ + fmap normalizeLine $ + (maybeToList <$> optional pathText) <> (concat <$> some pathTraversal) uri :: Parser [[StringPart]] -uri = fmap (pure . pure . TextPart) $ try $ - someP schemeChar <> chunk ":" <> someP uriChar +uri = + fmap (pure . pure . TextPart) $ + try $ + someP schemeChar <> chunk ":" <> someP uriChar -- STRINGS interpolation :: Parser StringPart -interpolation = Interpolation <$> - (rawSymbol TInterOpen *> lift (whole expression) <* rawSymbol TInterClose) +interpolation = + Interpolation + <$> (rawSymbol TInterOpen *> lift (whole expression) <* rawSymbol TInterClose) -- Interpolation, but only allowing identifiers and simple strings inside interpolationRestricted :: Parser StringPart @@ -118,59 +177,68 @@ interpolationRestricted = do _ -> empty simpleStringPart :: Parser StringPart -simpleStringPart = TextPart <$> someText ( - chunk "\\n" <|> - chunk "\\r" <|> - chunk "\\t" <|> - ((<>) <$> chunk "\\" <*> (Text.singleton <$> anySingle)) <|> - chunk "$$" <|> - try (chunk "$" <* notFollowedBy (char '{')) <|> - someP (\t -> t /= '"' && t /= '\\' && t /= '$')) +simpleStringPart = + TextPart + <$> someText + ( chunk "\\n" + <|> chunk "\\r" + <|> chunk "\\t" + <|> ((<>) <$> chunk "\\" <*> (Text.singleton <$> anySingle)) + <|> chunk "$$" + <|> try (chunk "$" <* notFollowedBy (char '{')) + <|> someP (\t -> t /= '"' && t /= '\\' && t /= '$') + ) indentedStringPart :: Parser StringPart -indentedStringPart = TextPart <$> someText ( - chunk "''\\n" <|> - chunk "''\\r" <|> - chunk "''\\t" <|> - chunk "''\\" *> (Text.singleton <$> anySingle) <|> - chunk "''$" <|> - chunk "'''" <|> - chunk "$$" <|> - try (chunk "$" <* notFollowedBy (char '{')) <|> - try (chunk "'" <* notFollowedBy (char '\'')) <|> - someP (\t -> t /= '\'' && t /= '$' && t /= '\n')) +indentedStringPart = + TextPart + <$> someText + ( chunk "''\\n" + <|> chunk "''\\r" + <|> chunk "''\\t" + <|> chunk "''\\" + *> (Text.singleton <$> anySingle) + <|> chunk "''$" + <|> chunk "'''" + <|> chunk "$$" + <|> try (chunk "$" <* notFollowedBy (char '{')) + <|> try (chunk "'" <* notFollowedBy (char '\'')) + <|> someP (\t -> t /= '\'' && t /= '$' && t /= '\n') + ) indentedLine :: Parser [StringPart] indentedLine = many (indentedStringPart <|> interpolation) isEmptyLine :: [StringPart] -> Bool -isEmptyLine [] = True +isEmptyLine [] = True isEmptyLine [TextPart t] = isSpaces t -isEmptyLine _ = False +isEmptyLine _ = False -- | Drop the first line of a string if it is empty. -- However, don't drop it if it is the only line (empty string) fixFirstLine :: [[StringPart]] -> [[StringPart]] -fixFirstLine [] = [] +fixFirstLine [] = [] fixFirstLine (x : xs) = if isEmptyLine x' && not (null xs) then xs else x' : xs - where x' = normalizeLine x + where + x' = normalizeLine x -- | Empty the last line if it contains only spaces. fixLastLine :: [[StringPart]] -> [[StringPart]] -fixLastLine [] = [] -fixLastLine [line] = if isEmptyLine line' then [[]] else [line'] - where line' = normalizeLine line +fixLastLine [] = [] +fixLastLine [line] = if isEmptyLine line' then [[]] else [line'] + where + line' = normalizeLine line fixLastLine (x : xs) = x : fixLastLine xs lineHead :: [StringPart] -> Maybe Text -lineHead [] = Nothing -lineHead line | isEmptyLine line = Nothing -lineHead (TextPart t : _) = Just t +lineHead [] = Nothing +lineHead line | isEmptyLine line = Nothing +lineHead (TextPart t : _) = Just t lineHead (Interpolation{} : _) = Just "" stripParts :: Text -> [StringPart] -> [StringPart] stripParts indentation (TextPart t : xs) = - TextPart (fromMaybe Text.empty $ Text.stripPrefix indentation t) : xs + TextPart (fromMaybe Text.empty $ Text.stripPrefix indentation t) : xs stripParts _ xs = xs -- | Split a list of StringParts on the newlines in their TextParts. @@ -178,20 +246,19 @@ stripParts _ xs = xs splitLines :: [StringPart] -> [[StringPart]] splitLines [] = [[]] splitLines (TextPart t : xs) = - let ts = map (pure . TextPart) $ Text.split (=='\n') t - in case splitLines xs of - (xs' : xss) -> init ts ++ ((last ts ++ xs') : xss) - _ -> error "unreachable" - + let ts = map (pure . TextPart) $ Text.split (== '\n') t + in case splitLines xs of + (xs' : xss) -> init ts ++ ((last ts ++ xs') : xss) + _ -> error "unreachable" splitLines (x : xs) = - case splitLines xs of - (xs' : xss) -> (x : xs') : xss - _ -> error "unreachable" + case splitLines xs of + (xs' : xss) -> (x : xs') : xss + _ -> error "unreachable" stripIndentation :: [[StringPart]] -> [[StringPart]] stripIndentation parts = case commonIndentation $ mapMaybe lineHead parts of - Nothing -> map (const []) parts - Just indentation -> map (stripParts indentation) parts + Nothing -> map (const []) parts + Just indentation -> map (stripParts indentation) parts normalizeLine :: [StringPart] -> [StringPart] normalizeLine [] = [] @@ -203,41 +270,51 @@ fixSimpleString :: [StringPart] -> [[StringPart]] fixSimpleString = map normalizeLine . splitLines simpleString :: Parser [[StringPart]] -simpleString = rawSymbol TDoubleQuote *> - fmap fixSimpleString (many (simpleStringPart <|> interpolation)) <* - rawSymbol TDoubleQuote +simpleString = + rawSymbol TDoubleQuote + *> fmap fixSimpleString (many (simpleStringPart <|> interpolation)) + <* rawSymbol TDoubleQuote fixIndentedString :: [[StringPart]] -> [[StringPart]] -fixIndentedString - = map normalizeLine +fixIndentedString = + map normalizeLine . concatMap splitLines . stripIndentation . fixLastLine . fixFirstLine indentedString :: Parser [[StringPart]] -indentedString = rawSymbol TDoubleSingleQuote *> - fmap fixIndentedString (sepBy indentedLine (chunk "\n")) <* - rawSymbol TDoubleSingleQuote +indentedString = + rawSymbol TDoubleSingleQuote + *> fmap fixIndentedString (sepBy indentedLine (chunk "\n")) + <* rawSymbol TDoubleSingleQuote + -- TERMS parens :: Parser Term -parens = Parenthesized <$> - symbol TParenOpen <*> expression <*> symbol TParenClose +parens = + Parenthesized + <$> symbol TParenOpen + <*> expression + <*> symbol TParenClose simpleSelector :: Parser StringPart -> Parser SimpleSelector simpleSelector parseInterpolation = - (IDSelector <$> identifier) <|> - (InterpolSelector <$> lexeme parseInterpolation) <|> - (StringSelector <$> lexeme simpleString) + (IDSelector <$> identifier) + <|> (InterpolSelector <$> lexeme parseInterpolation) + <|> (StringSelector <$> lexeme simpleString) selector :: Maybe (Parser Leaf) -> Parser Selector -selector parseDot = Selector <$> - sequence parseDot <* notFollowedBy path <*> simpleSelector interpolation +selector parseDot = + Selector + <$> sequence parseDot + <* notFollowedBy path + <*> simpleSelector interpolation selectorPath :: Parser [Selector] -selectorPath = (pure <$> selector Nothing) <> - many (selector $ Just $ symbol TDot) +selectorPath = + (pure <$> selector Nothing) + <> many (selector $ Just $ symbol TDot) -- Path with a leading dot selectorPath' :: Parser [Selector] @@ -246,7 +323,7 @@ selectorPath' = many $ try $ selector $ Just $ symbol TDot -- Everything but selection simpleTerm :: Parser Term simpleTerm = - (SimpleString <$> lexeme (simpleString <|> uri)) + (SimpleString <$> lexeme (simpleString <|> uri)) <|> (IndentedString <$> lexeme indentedString) <|> (Path <$> path) <|> (Token <$> (envPath <|> float <|> integer <|> identifier)) @@ -256,12 +333,12 @@ simpleTerm = term :: Parser Term term = label "term" $ do - t <- simpleTerm - sel <- selectorPath' - def <- optional (liftM2 (,) (reserved KOr) term) - return $ case sel of - [] -> t - _ -> Selection t sel def + t <- simpleTerm + sel <- selectorPath' + def <- optional (liftM2 (,) (reserved KOr) term) + return $ case sel of + [] -> t + _ -> Selection t sel def items :: Parser a -> Parser (Items a) items p = Items <$> many (item p) <> (toList <$> optional lastItem) @@ -271,65 +348,84 @@ item p = detachedComment <|> CommentedItem <$> takeTrivia <*> p lastItem :: Parser (Item a) lastItem = do - trivia <- takeTrivia - case trivia of - [] -> empty - _ -> pure $ DetachedComments trivia + trivia <- takeTrivia + case trivia of + [] -> empty + _ -> pure $ DetachedComments trivia detachedComment :: Parser (Item a) detachedComment = do - trivia <- takeTrivia - case break (== EmptyLine) trivia of - -- Return a set of comments that don't annotate the next item - (detached, EmptyLine : trivia') -> pushTrivia trivia' >> pure (DetachedComments detached) - -- The remaining trivia annotate the next item - _ -> pushTrivia trivia >> empty + trivia <- takeTrivia + case break (== EmptyLine) trivia of + -- Return a set of comments that don't annotate the next item + (detached, EmptyLine : trivia') -> pushTrivia trivia' >> pure (DetachedComments detached) + -- The remaining trivia annotate the next item + _ -> pushTrivia trivia >> empty -- ABSTRACTIONS attrParameter :: Maybe (Parser Leaf) -> Parser ParamAttr -attrParameter parseComma = ParamAttr <$> - identifier <*> optional (liftM2 (,) (symbol TQuestion) expression) <*> - sequence parseComma +attrParameter parseComma = + ParamAttr + <$> identifier + <*> optional (liftM2 (,) (symbol TQuestion) expression) + <*> sequence parseComma idParameter :: Parser Parameter idParameter = IDParameter <$> identifier setParameter :: Parser Parameter setParameter = SetParameter <$> bopen <*> attrs <*> bclose - where bopen = symbol TBraceOpen - bclose = symbol TBraceClose - commaAttrs = many $ try $ attrParameter $ Just $ symbol TComma - ellipsis = ParamEllipsis <$> symbol TEllipsis - lastAttr = attrParameter Nothing <|> ellipsis - attrs = commaAttrs <> (toList <$> optional lastAttr) + where + bopen = symbol TBraceOpen + bclose = symbol TBraceClose + commaAttrs = many $ try $ attrParameter $ Just $ symbol TComma + ellipsis = ParamEllipsis <$> symbol TEllipsis + lastAttr = attrParameter Nothing <|> ellipsis + attrs = commaAttrs <> (toList <$> optional lastAttr) contextParameter :: Parser Parameter contextParameter = - try (ContextParameter <$> setParameter <*> symbol TAt <*> idParameter) <|> - try (ContextParameter <$> idParameter <*> symbol TAt <*> setParameter) + try (ContextParameter <$> setParameter <*> symbol TAt <*> idParameter) + <|> try (ContextParameter <$> idParameter <*> symbol TAt <*> setParameter) abstraction :: Parser Expression -abstraction = try (Abstraction <$> - (contextParameter <|> setParameter <|> idParameter) <*> - symbol TColon) <*> expression +abstraction = + try + ( Abstraction + <$> (contextParameter <|> setParameter <|> idParameter) + <*> symbol TColon + ) + <*> expression -- SETS AND LISTS inherit :: Parser Binder -inherit = Inherit <$> reserved KInherit <*> optional parens <*> - many (simpleSelector interpolationRestricted) <*> symbol TSemicolon +inherit = + Inherit + <$> reserved KInherit + <*> optional parens + <*> many (simpleSelector interpolationRestricted) + <*> symbol TSemicolon assignment :: Parser Binder -assignment = Assignment <$> - selectorPath <*> symbol TAssign <*> expression <*> symbol TSemicolon +assignment = + Assignment + <$> selectorPath + <*> symbol TAssign + <*> expression + <*> symbol TSemicolon binders :: Parser (Items Binder) binders = items (assignment <|> inherit) set :: Parser Term -set = Set <$> optional (reserved KRec <|> reserved KLet) <*> - symbol TBraceOpen <*> binders <*> symbol TBraceClose +set = + Set + <$> optional (reserved KRec <|> reserved KLet) + <*> symbol TBraceOpen + <*> binders + <*> symbol TBraceClose list :: Parser Term list = List <$> symbol TBrackOpen <*> items term <*> symbol TBrackClose @@ -337,64 +433,87 @@ list = List <$> symbol TBrackOpen <*> items term <*> symbol TBrackClose -- OPERATORS operator :: Token -> Parser Leaf -operator t = label "operator" $ try $ lexeme $ - rawSymbol t <* notFollowedBy (oneOf ( - -- Resolve ambiguities between operators which are prefixes of others - case t of - TPlus -> "+" :: [Char] - TMinus -> ">" - TMul -> "/" - TDiv -> "/*" - TLess -> "=" - TGreater -> "=" - TNot -> "=" - _ -> "" - )) +operator t = + label "operator" $ + try $ + lexeme $ + rawSymbol t + <* notFollowedBy + ( oneOf + ( -- Resolve ambiguities between operators which are prefixes of others + case t of + TPlus -> "+" :: [Char] + TMinus -> ">" + TMul -> "/" + TDiv -> "/*" + TLess -> "=" + TGreater -> "=" + TNot -> "=" + _ -> "" + ) + ) opCombiner :: Operator -> MPExpr.Operator Parser Expression opCombiner Apply = MPExpr.InfixL $ return Application - opCombiner (Op Prefix TMinus) = MPExpr.Prefix $ Negation <$> operator TMinus -opCombiner (Op Prefix TNot) = MPExpr.Prefix $ Inversion <$> operator TNot -opCombiner (Op Prefix _) = undefined - -opCombiner (Op Postfix TQuestion) = MPExpr.Postfix $ - (\question sel expr -> MemberCheck expr question sel) <$> - operator TQuestion <*> selectorPath - +opCombiner (Op Prefix TNot) = MPExpr.Prefix $ Inversion <$> operator TNot +opCombiner (Op Prefix _) = undefined +opCombiner (Op Postfix TQuestion) = + MPExpr.Postfix $ + (\question sel expr -> MemberCheck expr question sel) + <$> operator TQuestion + <*> selectorPath opCombiner (Op Postfix _) = undefined - opCombiner (Op InfixL tok) = MPExpr.InfixL $ flip Operation <$> operator tok opCombiner (Op InfixN tok) = MPExpr.InfixN $ flip Operation <$> operator tok opCombiner (Op InfixR tok) = MPExpr.InfixR $ flip Operation <$> operator tok operation :: Parser Expression -operation = MPExpr.makeExprParser +operation = + MPExpr.makeExprParser (Term <$> term <* notFollowedBy (oneOf (":@" :: [Char]))) (map (map opCombiner) operators) -- EXPRESSIONS with :: Parser Expression -with = With <$> - reserved KWith <*> expression <*> symbol TSemicolon <*> expression +with = + With + <$> reserved KWith + <*> expression + <*> symbol TSemicolon + <*> expression letIn :: Parser Expression letIn = Let <$> reserved KLet <*> binders <*> reserved KIn <*> expression ifThenElse :: Parser Expression -ifThenElse = If <$> - reserved KIf <*> expression <*> - reserved KThen <*> expression <*> - reserved KElse <*> expression +ifThenElse = + If + <$> reserved KIf + <*> expression + <*> reserved KThen + <*> expression + <*> reserved KElse + <*> expression assert :: Parser Expression -assert = Assert <$> reserved KAssert <*> expression <*> - symbol TSemicolon <*> expression +assert = + Assert + <$> reserved KAssert + <*> expression + <*> symbol TSemicolon + <*> expression expression :: Parser Expression -expression = label "expression" $ try operation <|> abstraction <|> - with <|> letIn <|> ifThenElse <|> assert +expression = + label "expression" $ + try operation + <|> abstraction + <|> with + <|> letIn + <|> ifThenElse + <|> assert file :: Parsec Void Text File file = whole (expression <* eof) diff --git a/src/Nixfmt/Parser/Float.hs b/src/Nixfmt/Parser/Float.hs index 31ba89c4..00e403dc 100644 --- a/src/Nixfmt/Parser/Float.hs +++ b/src/Nixfmt/Parser/Float.hs @@ -1,41 +1,53 @@ -{-# LANGUAGE TypeFamilies, TypeApplications, ScopedTypeVariables, TypeOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} module Nixfmt.Parser.Float (floatParse) where +import "base" Control.Monad (void) +import qualified "base" Data.Char as Char import "base" Data.Foldable (foldl') import "base" Data.Proxy (Proxy (..)) -import qualified "base" Data.Char as Char - -import "base" Control.Monad (void) import "megaparsec" Text.Megaparsec ( - option, chunkToTokens, takeWhile1P, try, notFollowedBy, - (<|>), (), MonadParsec, Token, - ) -import "megaparsec" Text.Megaparsec.Char.Lexer (decimal, signed) + MonadParsec, + Token, + chunkToTokens, + notFollowedBy, + option, + takeWhile1P, + try, + (), + (<|>), + ) import "megaparsec" Text.Megaparsec.Char (char, char', digitChar) - -import "scientific" Data.Scientific (toRealFloat, scientific) +import "megaparsec" Text.Megaparsec.Char.Lexer (decimal, signed) +import "scientific" Data.Scientific (scientific, toRealFloat) -- copied (and modified) from Text.Megaparsec.Char.Lexer data SP = SP !Integer {-# UNPACK #-} !Int + floatParse :: (MonadParsec e s m, Token s ~ Char, RealFloat a) => m a floatParse = do notFollowedBy $ char '0' >> digitChar notFollowedBy (char' 'e') c' <- (decimal "decimal") <|> return 0 toRealFloat - <$> (( do + <$> ( ( do SP c e' <- dotDecimal_ c' e <- option e' (try $ exponent_ e') return (scientific c e) - ) + ) <|> (scientific c' <$> exponent_ 0) ) {-# INLINE floatParse #-} -- copied from Text.Megaparsec.Char.Lexer -dotDecimal_ :: forall e s m. - (MonadParsec e s m, Token s ~ Char) => Integer -> m SP +dotDecimal_ :: + forall e s m. + (MonadParsec e s m, Token s ~ Char) => + Integer -> + m SP dotDecimal_ c' = do void (char '.') let mkNum = foldl' step (SP c' 0) . chunkToTokens @s Proxy @@ -52,4 +64,3 @@ exponent_ e' = do void (char' 'e') (+ e') <$> signed (return ()) decimal {-# INLINE exponent_ #-} - diff --git a/src/Nixfmt/Predoc.hs b/src/Nixfmt/Predoc.hs index 60a9c868..2e91402f 100644 --- a/src/Nixfmt/Predoc.hs +++ b/src/Nixfmt/Predoc.hs @@ -1,78 +1,79 @@ -{-# LANGUAGE FlexibleInstances, OverloadedStrings #-} - -module Nixfmt.Predoc - ( text - , comment - , trailingComment - , trailing - , sepBy - , surroundWith - , hcat - , group - , group' - , nest - , offset - , softline' - , line' - , softline - , line - , hardspace - , hardline - , emptyline - , newline - , DocE - , Doc - , GroupAnn(..) - , Pretty - , pretty - , fixup - , unexpandSpacing' - , layout - , textWidth - ) where - -import Data.List (intersperse) -import qualified Data.List.NonEmpty as NonEmpty -import Data.List.NonEmpty (NonEmpty(..), singleton, (<|)) +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} + +module Nixfmt.Predoc ( + text, + comment, + trailingComment, + trailing, + sepBy, + surroundWith, + hcat, + group, + group', + nest, + offset, + softline', + line', + softline, + line, + hardspace, + hardline, + emptyline, + newline, + DocE, + Doc, + GroupAnn (..), + Pretty, + pretty, + fixup, + unexpandSpacing', + layout, + textWidth, +) +where + +import Control.Applicative (asum, empty, (<|>)) +import Control.Monad.Trans.State.Strict (State, StateT (..), evalState, get, mapStateT, modify, put, runState, state) +import Data.Bifunctor (first, second) import Data.Function ((&)) -import Data.Functor ((<&>), ($>)) +import Data.Functor (($>), (<&>)) import Data.Functor.Identity (runIdentity) -import Data.Bifunctor (first, second) +import Data.List (intersperse) +import Data.List.NonEmpty (NonEmpty (..), singleton, (<|)) +import qualified Data.List.NonEmpty as NonEmpty import Data.Maybe (fromMaybe) import Data.Text as Text (Text, concat, length, replicate, strip) import GHC.Stack (HasCallStack) -import Control.Applicative ((<|>), asum, empty) -import Control.Monad.Trans.State.Strict (State, StateT, StateT(..), mapStateT, state, runState, evalState, get, put, modify) -- | Sequential Spacings are reduced to a single Spacing by taking the maximum. -- This means that e.g. a Space followed by an Emptyline results in just an -- Emptyline. data Spacing - = - -- | Line break or nothing (soft) + = -- | Line break or nothing (soft) Softbreak - -- | Line break or nothing - | Break - -- | Always a space - | Hardspace - -- | Line break or space (soft) - | Softspace - -- | Line break or space - | Space - -- | Always a line break - | Hardline - -- | Two line breaks - | Emptyline - -- | n line breaks - | Newlines Int - deriving (Show, Eq, Ord) + | -- | Line break or nothing + Break + | -- | Always a space + Hardspace + | -- | Line break or space (soft) + Softspace + | -- | Line break or space + Space + | -- | Always a line break + Hardline + | -- | Two line breaks + Emptyline + | -- | n line breaks + Newlines Int + deriving (Show, Eq, Ord) -- | `Group docs` indicates that either all or none of the Spaces and Breaks -- in `docs` should be converted to line breaks. This does not affect softlines, -- those will be expanded only as necessary and with a lower priority. data GroupAnn - = RegularG - -- Group with priority expansion. This is only rarely needed, and mostly useful + = RegularG + | -- Group with priority expansion. This is only rarely needed, and mostly useful -- to compact things left and right of a multiline element as long as they fit onto one line. -- -- Groups containing priority groups are treated as having three segments: @@ -85,14 +86,14 @@ data GroupAnn -- If a group contains multiple priority groups, then the renderer will attempt to expand them, -- each one individually, and in *reverse* order. If all of these fail, then the entire group -- will be fully expanded as if it didn't contain any priority groups. - | Priority - -- Usually, priority groups are associated and handled by their direct parent group. However, + Priority + | -- Usually, priority groups are associated and handled by their direct parent group. However, -- if the parent is a `Transparent` group, then they will be associated with its parent instead. -- (This goes on transitively until the first non-transparent parent group.) -- In the case of priority group expansion, this group will be treated as non-existent (transparent). -- Otherwise, it will be treated like a regular group. - | Transparent - deriving (Show, Eq) + Transparent + deriving (Show, Eq) -- Comments do not count towards some line length limits -- Trailing tokens have the property that they will only exist in expanded groups, and "swallowed" in compact groups @@ -100,47 +101,47 @@ data GroupAnn -- (The difference is that trailing comments are guaranteed to be single "# text" tokens, while all other comments -- may be composite and multi-line) data TextAnn = RegularT | Comment | TrailingComment | Trailing - deriving (Show, Eq) + deriving (Show, Eq) -- | Single document element. Documents are modeled as lists of these elements -- in order to make concatenation simple. -data DocE = - -- nesting depth, offset, kind, text +data DocE + = -- nesting depth, offset, kind, text Text Int Int TextAnn Text - | Spacing Spacing - | Group GroupAnn Doc - deriving (Show, Eq) + | Spacing Spacing + | Group GroupAnn Doc + deriving (Show, Eq) type Doc = [DocE] class Pretty a where - pretty :: a -> Doc + pretty :: a -> Doc instance Pretty Doc where - pretty = id + pretty = id -instance Pretty a => Pretty (Maybe a) where - pretty Nothing = mempty - pretty (Just x) = pretty x +instance (Pretty a) => Pretty (Maybe a) where + pretty Nothing = mempty + pretty (Just x) = pretty x instance (Pretty a, Pretty b) => Pretty (a, b) where - pretty (a, b) = pretty a <> pretty b + pretty (a, b) = pretty a <> pretty b instance (Pretty a, Pretty b, Pretty c) => Pretty (a, b, c) where - pretty (a, b, c) = pretty a <> pretty b <> pretty c + pretty (a, b, c) = pretty a <> pretty b <> pretty c text :: Text -> Doc text "" = [] -text t = [Text 0 0 RegularT t] +text t = [Text 0 0 RegularT t] comment :: Text -> Doc comment "" = [] -comment t = [Text 0 0 Comment t] +comment t = [Text 0 0 Comment t] -- Comment at the end of a line trailingComment :: Text -> Doc trailingComment "" = [] -trailingComment t = [Text 0 0 TrailingComment t] +trailingComment t = [Text 0 0 TrailingComment t] -- Text tokens that are only needed in expanded groups trailing :: Text -> Doc @@ -150,13 +151,14 @@ trailing t = [Text 0 0 Trailing t] -- | Group document elements together (see Node Group documentation) -- Must not contain non-hard whitespace (e.g. line, softline' etc.) at the start of the end. -- Use group' for that instead if you are sure of what you are doing. -group :: HasCallStack => Pretty a => a -> Doc -group x = pure . Group RegularG $ - if p /= [] && (isSoftSpacing (head p) || isSoftSpacing (last p)) then - error $ "group should not start or end with whitespace, use `group'` if you are sure; " <> show p - else - p - where p = pretty x +group :: (HasCallStack) => (Pretty a) => a -> Doc +group x = + pure . Group RegularG $ + if p /= [] && (isSoftSpacing (head p) || isSoftSpacing (last p)) + then error $ "group should not start or end with whitespace, use `group'` if you are sure; " <> show p + else p + where + p = pretty x -- | Group document elements together (see Node Group documentation) -- Is allowed to start or end with any kind of whitespace. @@ -165,7 +167,7 @@ group x = pure . Group RegularG $ -- or you'll get some *very* confusing bugs … -- -- Also allows to create priority groups (see Node Group documentation) -group' :: Pretty a => GroupAnn -> a -> Doc +group' :: (Pretty a) => GroupAnn -> a -> Doc group' ann = pure . Group ann . pretty -- | @nest doc@ declarse @doc@ to have a higher nesting depth @@ -173,18 +175,18 @@ group' ann = pure . Group ann . pretty -- this will be calculated automatically later on. As a rule of thumb: -- Multiple nesting levels on one line will be compacted and only result in a single -- indentation bump for the next line. This prevents excessive indentation. -nest :: Pretty a => a -> Doc +nest :: (Pretty a) => a -> Doc nest x = map go $ pretty x - where + where go (Text i o ann t) = Text (i + 1) o ann t go (Group ann inner) = Group ann (map go inner) go spacing = spacing -- This is similar to nest, however it circumvents the "smart" rules that usually apply. -- This should only be useful to manage the indentation within indented strings. -offset :: Pretty a => Int -> a -> Doc +offset :: (Pretty a) => Int -> a -> Doc offset level x = map go $ pretty x - where + where go (Text i o ann t) = Text i (o + level) ann t go (Group ann inner) = Group ann (map go inner) go spacing = spacing @@ -220,14 +222,14 @@ emptyline = [Spacing Emptyline] newline :: Doc newline = [Spacing (Newlines 1)] -surroundWith :: Pretty a => Doc -> a -> Doc +surroundWith :: (Pretty a) => Doc -> a -> Doc surroundWith outside inner = outside <> pretty inner <> outside -sepBy :: Pretty a => Doc -> [a] -> Doc +sepBy :: (Pretty a) => Doc -> [a] -> Doc sepBy separator = mconcat . intersperse separator . map pretty -- | Concatenate documents horizontally without spacing. -hcat :: Pretty a => [a] -> Doc +hcat :: (Pretty a) => [a] -> Doc hcat = mconcat . map pretty -- Everything that may change representation depending on grouping @@ -236,7 +238,7 @@ isSoftSpacing (Spacing Softbreak) = True isSoftSpacing (Spacing Break) = True isSoftSpacing (Spacing Softspace) = True isSoftSpacing (Spacing Space) = True -isSoftSpacing _ = False +isSoftSpacing _ = False -- Everything else isHardSpacing :: DocE -> Bool @@ -244,7 +246,7 @@ isHardSpacing (Spacing Hardspace) = True isHardSpacing (Spacing Hardline) = True isHardSpacing (Spacing Emptyline) = True isHardSpacing (Spacing (Newlines _)) = True -isHardSpacing _ = False +isHardSpacing _ = False -- Check if an element is a comment -- Some comments are nested as nodes with multiple elements. @@ -259,12 +261,12 @@ isComment _ = False --- Does not recurse into inner groups. unexpandSpacing :: Doc -> Doc unexpandSpacing [] = [] -unexpandSpacing ((Spacing Space):xs) = Spacing Hardspace : unexpandSpacing xs -unexpandSpacing ((Spacing Softspace):xs) = Spacing Hardspace : unexpandSpacing xs -unexpandSpacing ((Spacing Break):xs) = unexpandSpacing xs -unexpandSpacing ((Spacing Softbreak):xs) = unexpandSpacing xs -unexpandSpacing (s@(Spacing _):xs) = s : unexpandSpacing xs -unexpandSpacing (x:xs) = x : unexpandSpacing xs +unexpandSpacing ((Spacing Space) : xs) = Spacing Hardspace : unexpandSpacing xs +unexpandSpacing ((Spacing Softspace) : xs) = Spacing Hardspace : unexpandSpacing xs +unexpandSpacing ((Spacing Break) : xs) = unexpandSpacing xs +unexpandSpacing ((Spacing Softbreak) : xs) = unexpandSpacing xs +unexpandSpacing (s@(Spacing _) : xs) = s : unexpandSpacing xs +unexpandSpacing (x : xs) = x : unexpandSpacing xs spanEnd :: (a -> Bool) -> [a] -> ([a], [a]) spanEnd p = fmap reverse . span p . reverse @@ -276,14 +278,14 @@ spanEnd p = fmap reverse . span p . reverse unexpandSpacing' :: Maybe Int -> Doc -> Maybe Doc unexpandSpacing' (Just n) _ | n < 0 = Nothing unexpandSpacing' _ [] = Just [] -unexpandSpacing' n (txt@(Text _ _ _ t):xs) = (txt :) <$> unexpandSpacing' (n <&> subtract (textWidth t)) xs -unexpandSpacing' n (Spacing Hardspace:xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs -unexpandSpacing' n (Spacing Space:xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs -unexpandSpacing' n (Spacing Softspace:xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs -unexpandSpacing' n (Spacing Break:xs) = unexpandSpacing' n xs -unexpandSpacing' n (Spacing Softbreak:xs) = unexpandSpacing' n xs -unexpandSpacing' _ (Spacing _:_) = Nothing -unexpandSpacing' n ((Group _ xs):ys) = unexpandSpacing' n $ xs <> ys +unexpandSpacing' n (txt@(Text _ _ _ t) : xs) = (txt :) <$> unexpandSpacing' (n <&> subtract (textWidth t)) xs +unexpandSpacing' n (Spacing Hardspace : xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs +unexpandSpacing' n (Spacing Space : xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs +unexpandSpacing' n (Spacing Softspace : xs) = (Spacing Hardspace :) <$> unexpandSpacing' (n <&> subtract 1) xs +unexpandSpacing' n (Spacing Break : xs) = unexpandSpacing' n xs +unexpandSpacing' n (Spacing Softbreak : xs) = unexpandSpacing' n xs +unexpandSpacing' _ (Spacing _ : _) = Nothing +unexpandSpacing' n ((Group _ xs) : ys) = unexpandSpacing' n $ xs <> ys -- Dissolve some groups with only one item simplifyGroup :: GroupAnn -> Doc -> Doc @@ -308,44 +310,40 @@ fixup (Spacing a : Spacing b : xs) = fixup $ Spacing (mergeSpacings a b) : xs -- Merge consecutive texts. Take indentation and offset from the left one fixup (Text level off ann a : Text _ _ ann' b : xs) | ann == ann' = fixup $ Text level off ann (a <> b) : xs -- Move/Merge hard spaces into groups -fixup ((Spacing Hardspace) : Group ann xs : ys) = fixup $ Group ann (Spacing Hardspace:xs) : ys +fixup ((Spacing Hardspace) : Group ann xs : ys) = fixup $ Group ann (Spacing Hardspace : xs) : ys -- Handle group, with stuff in front of it to potentially merge with fixup (a@(Spacing _) : Group ann xs : ys) = - let - -- Recurse onto xs, split out leading and trailing whitespace into pre and post. - -- For the leading side, also move out comments out of groups, they are kinda the same thing - -- (We could move out trailing comments too but it would make no difference) - (pre, rest) = span (\x -> isHardSpacing x || isComment x) $ fixup xs - (post, body) = second (simplifyGroup ann) $ spanEnd isHardSpacing rest - in if null body then - -- Dissolve empty group + let -- Recurse onto xs, split out leading and trailing whitespace into pre and post. + -- For the leading side, also move out comments out of groups, they are kinda the same thing + -- (We could move out trailing comments too but it would make no difference) + (pre, rest) = span (\x -> isHardSpacing x || isComment x) $ fixup xs + (post, body) = second (simplifyGroup ann) $ spanEnd isHardSpacing rest + in if null body + then -- Dissolve empty group fixup $ (a : pre) ++ post ++ ys - else - fixup (a : pre) ++ [Group ann body] ++ fixup (post ++ ys) + else fixup (a : pre) ++ [Group ann body] ++ fixup (post ++ ys) -- Handle group, almost the same thing as above fixup (Group ann xs : ys) = - let - (pre, rest) = span (\x -> isHardSpacing x || isComment x) $ fixup xs - (post, body) = second (simplifyGroup ann) $ spanEnd isHardSpacing rest - in if null body then - fixup $ pre ++ post ++ ys - else - fixup pre ++ [Group ann body] ++ fixup (post ++ ys) + let (pre, rest) = span (\x -> isHardSpacing x || isComment x) $ fixup xs + (post, body) = second (simplifyGroup ann) $ spanEnd isHardSpacing rest + in if null body + then fixup $ pre ++ post ++ ys + else fixup pre ++ [Group ann body] ++ fixup (post ++ ys) fixup (x : xs) = x : fixup xs mergeSpacings :: Spacing -> Spacing -> Spacing -mergeSpacings x y | x > y = mergeSpacings y x -mergeSpacings Break Softspace = Space -mergeSpacings Break Hardspace = Space -mergeSpacings Softbreak Hardspace = Softspace +mergeSpacings x y | x > y = mergeSpacings y x +mergeSpacings Break Softspace = Space +mergeSpacings Break Hardspace = Space +mergeSpacings Softbreak Hardspace = Softspace mergeSpacings (Newlines x) (Newlines y) = Newlines (x + y) -mergeSpacings Emptyline (Newlines x) = Newlines (x + 2) -mergeSpacings Hardspace (Newlines x) = Newlines x -mergeSpacings _ (Newlines x) = Newlines (x + 1) -mergeSpacings _ y = y +mergeSpacings Emptyline (Newlines x) = Newlines (x + 2) +mergeSpacings Hardspace (Newlines x) = Newlines x +mergeSpacings _ (Newlines x) = Newlines (x + 1) +mergeSpacings _ y = y -layout :: Pretty a => Int -> a -> Text -layout w = (<>"\n") . Text.strip . layoutGreedy w . fixup . pretty +layout :: (Pretty a) => Int -> a -> Text +layout w = (<> "\n") . Text.strip . layoutGreedy w . fixup . pretty -- 1. Move and merge Spacings. -- 2. Convert Softlines to Grouped Lines and Hardspaces to Texts. @@ -353,35 +351,34 @@ layout w = (<>"\n") . Text.strip . layoutGreedy w . fixup . pretty -- 4. For each Group, if it fits on a single line, render it that way. -- 5. If not, convert lines to hardlines and unwrap the group - -- Extract and list the priority groups of this group. -- The return value is a segmentation of the input, each segment annotated with its priority (True = Priority). -- This recurses into `Transparent` subgroups on the search for priority groups, and flattens their content in the output. -- If no priority groups are found, the empty list is returned. priorityGroups :: Doc -> [(Doc, Doc, Doc)] priorityGroups = explode . mergeSegments . segments - where + where segments :: Doc -> [(Bool, Doc)] segments [] = [] - segments ((Group Priority ys):xs) = (True, ys) : segments xs - segments ((Group Transparent ys):xs) = segments ys ++ segments xs - segments (x:xs) = (False, pure x) : segments xs + segments ((Group Priority ys) : xs) = (True, ys) : segments xs + segments ((Group Transparent ys) : xs) = segments ys ++ segments xs + segments (x : xs) = (False, pure x) : segments xs -- Merge subsequent segments of non-priority-group elements mergeSegments :: [(Bool, Doc)] -> [(Bool, Doc)] mergeSegments [] = [] - mergeSegments ((False, content1):(False, content2):xs) = mergeSegments $ (False, content1 ++ content2):xs - mergeSegments (x:xs) = x : mergeSegments xs + mergeSegments ((False, content1) : (False, content2) : xs) = mergeSegments $ (False, content1 ++ content2) : xs + mergeSegments (x : xs) = x : mergeSegments xs -- Convert the segmented/pre-porcessed input into a list of all groups as (pre, prio, post) triples explode :: [(Bool, Doc)] -> [(Doc, Doc, Doc)] explode [] = [] explode [(prio, x)] - | prio = [([], x, [])] - | otherwise = [] - explode ((prio, x):xs) - | prio = ([], x, concatMap snd xs) : map (\(a, b, c) -> (x<>a, b, c)) (explode xs) - | otherwise = map (\(a, b, c) -> (x<>a, b, c)) (explode xs) + | prio = [([], x, [])] + | otherwise = [] + explode ((prio, x) : xs) + | prio = ([], x, concatMap snd xs) : map (\(a, b, c) -> (x <> a, b, c)) (explode xs) + | otherwise = map (\(a, b, c) -> (x <> a, b, c)) (explode xs) -- | To support i18n, this function needs to be patched. textWidth :: Text -> Int @@ -396,61 +393,63 @@ fits _ c _ | c < 0 = Nothing fits _ _ [] = Just "" -- This case is impossible in the input thanks to fixup, but may happen -- due to our recursion on nodes below -fits ni c (Spacing a:Spacing b:xs) = fits ni c (Spacing (mergeSpacings a b):xs) -fits ni c (x:xs) = case x of - Text _ _ RegularT t -> (t<>) <$> fits (ni - textWidth t) (c - textWidth t) xs - Text _ _ Comment t -> (t<>) <$> fits ni c xs - Text _ _ TrailingComment t | ni == 0 -> ((" " <> t) <>) <$> fits ni c xs - | otherwise -> (t<>) <$> fits ni c xs - Text _ _ Trailing _ -> fits ni c xs - Spacing Softbreak -> fits ni c xs - Spacing Break -> fits ni c xs - Spacing Softspace -> (" "<>) <$> fits (ni - 1) (c - 1) xs - Spacing Space -> (" "<>) <$> fits (ni - 1) (c - 1) xs - Spacing Hardspace -> (" "<>) <$> fits (ni - 1) (c - 1) xs - Spacing Hardline -> Nothing - Spacing Emptyline -> Nothing - Spacing (Newlines _) -> Nothing - Group _ ys -> fits ni c $ ys ++ xs +fits ni c (Spacing a : Spacing b : xs) = fits ni c (Spacing (mergeSpacings a b) : xs) +fits ni c (x : xs) = case x of + Text _ _ RegularT t -> (t <>) <$> fits (ni - textWidth t) (c - textWidth t) xs + Text _ _ Comment t -> (t <>) <$> fits ni c xs + Text _ _ TrailingComment t + | ni == 0 -> ((" " <> t) <>) <$> fits ni c xs + | otherwise -> (t <>) <$> fits ni c xs + Text _ _ Trailing _ -> fits ni c xs + Spacing Softbreak -> fits ni c xs + Spacing Break -> fits ni c xs + Spacing Softspace -> (" " <>) <$> fits (ni - 1) (c - 1) xs + Spacing Space -> (" " <>) <$> fits (ni - 1) (c - 1) xs + Spacing Hardspace -> (" " <>) <$> fits (ni - 1) (c - 1) xs + Spacing Hardline -> Nothing + Spacing Emptyline -> Nothing + Spacing (Newlines _) -> Nothing + Group _ ys -> fits ni c $ ys ++ xs -- | Find the width of the first line in a list of documents, using target -- width 0, which always forces line breaks when possible. firstLineWidth :: Doc -> Int -firstLineWidth [] = 0 -firstLineWidth (Text _ _ Comment _ : xs) = firstLineWidth xs +firstLineWidth [] = 0 +firstLineWidth (Text _ _ Comment _ : xs) = firstLineWidth xs firstLineWidth (Text _ _ TrailingComment _ : xs) = firstLineWidth xs -firstLineWidth (Text _ _ _ t : xs) = textWidth t + firstLineWidth xs +firstLineWidth (Text _ _ _ t : xs) = textWidth t + firstLineWidth xs -- This case is impossible in the input thanks to fixup, but may happen -- due to our recursion on groups below -firstLineWidth (Spacing a : Spacing b : xs) = firstLineWidth (Spacing (mergeSpacings a b):xs) +firstLineWidth (Spacing a : Spacing b : xs) = firstLineWidth (Spacing (mergeSpacings a b) : xs) firstLineWidth (Spacing Hardspace : xs) = 1 + firstLineWidth xs -firstLineWidth (Spacing _ : _) = 0 -firstLineWidth (Group _ xs : ys) = firstLineWidth $ xs ++ ys +firstLineWidth (Spacing _ : _) = 0 +firstLineWidth (Group _ xs : ys) = firstLineWidth $ xs ++ ys -- | Check if the first line in a document fits a target width given -- a maximum width, without breaking up groups. firstLineFits :: Int -> Int -> Doc -> Bool firstLineFits targetWidth maxWidth docs = go maxWidth docs - where go c _ | c < 0 = False - go c [] = maxWidth - c <= targetWidth - go c (Text _ _ RegularT t : xs) = go (c - textWidth t) xs - go c (Text {} : xs) = go c xs - -- This case is impossible in the input thanks to fixup, but may happen - -- due to our recursion on groups below - go c (Spacing a : Spacing b : xs) = go c $ Spacing (mergeSpacings a b) : xs - go c (Spacing Hardspace : xs) = go (c - 1) xs - go c (Spacing _ : _) = maxWidth - c <= targetWidth - go c (Group _ ys : xs) = - case fits 0 (c - firstLineWidth xs) ys of - Nothing -> go c (ys ++ xs) - Just t -> go (c - textWidth t) xs + where + go c _ | c < 0 = False + go c [] = maxWidth - c <= targetWidth + go c (Text _ _ RegularT t : xs) = go (c - textWidth t) xs + go c (Text{} : xs) = go c xs + -- This case is impossible in the input thanks to fixup, but may happen + -- due to our recursion on groups below + go c (Spacing a : Spacing b : xs) = go c $ Spacing (mergeSpacings a b) : xs + go c (Spacing Hardspace : xs) = go (c - 1) xs + go c (Spacing _ : _) = maxWidth - c <= targetWidth + go c (Group _ ys : xs) = + case fits 0 (c - firstLineWidth xs) ys of + Nothing -> go c (ys ++ xs) + Just t -> go (c - textWidth t) xs -- Calculate the amount of indentation until the first token -- This assumes the input to be an unexpanded group at the start of a new line nextIndent :: Doc -> (Int, Int) nextIndent ((Text i o _ _) : _) = (i, o) nextIndent ((Group _ xs) : _) = nextIndent xs -nextIndent (_:xs) = nextIndent xs +nextIndent (_ : xs) = nextIndent xs nextIndent _ = (0, 0) -- | Create `n` newlines @@ -471,7 +470,7 @@ type St = (Int, NonEmpty (Int, Int)) -- tw Target Width layoutGreedy :: Int -> Doc -> Text layoutGreedy tw doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, singleton (0, 0)) - where + where -- Simple helpers around `put` with a tuple state putL = modify . first . const putR = modify . second . const @@ -479,9 +478,10 @@ layoutGreedy tw doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, s -- Print a given text. If this is the first token on a line, it will -- do the appropriate calculations for indentation and print that in addition to the text. putText :: Int -> Int -> Text -> State St [Text] - putText textNL textOffset t = get >>= - \(cc, indents@((ci, nl) :| indents')) -> - case textNL `compare` nl of + putText textNL textOffset t = + get + >>= \(cc, indents@((ci, nl) :| indents')) -> + case textNL `compare` nl of -- Push the textNL onto the stack, but only increase the actual indentation (`ci`) -- if this is the first one of a line. All subsequent nestings within the line effectively get "swallowed" GT -> putR ((if cc == 0 then ci + 2 else ci, textNL) <| indents) >> go' @@ -489,111 +489,108 @@ layoutGreedy tw doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, s -- Just pop from the stack and recurse until the indent matches again LT -> putR (NonEmpty.fromList indents') >> putText textNL textOffset t EQ -> go' - where - -- Put the text and advance the `cc` cursor. Add the appropriate amount of indentation if this is - -- the first token on a line - go' = do - (cc, (ci, _) :| _) <- get - putL (cc + textWidth t) - pure $ if cc == 0 then [indent (ci + textOffset), t] else [t] + where + -- Put the text and advance the `cc` cursor. Add the appropriate amount of indentation if this is + -- the first token on a line + go' = do + (cc, (ci, _) :| _) <- get + putL (cc + textWidth t) + pure $ if cc == 0 then [indent (ci + textOffset), t] else [t] -- Simply put text without caring about line-start indentation putText' :: [Text] -> State St [Text] putText' ts = do - (cc, indents) <- get - put (cc + sum (map textWidth ts), indents) - pure ts + (cc, indents) <- get + put (cc + sum (map textWidth ts), indents) + pure ts -- First argument: chunks to render -- Second argument: lookahead of following chunks, not rendered go :: Doc -> Doc -> State St [Text] go [] _ = return [] - go (x:xs) ys = do { t <- goOne x (xs ++ ys); ts <- go xs ys; return (t ++ ts) } + go (x : xs) ys = do t <- goOne x (xs ++ ys); ts <- go xs ys; return (t ++ ts) -- First argument: chunk to render. This will recurse into nests/groups if the chunk is one. -- Second argument: lookahead of following chunks goOne :: DocE -> Doc -> State St [Text] - goOne x xs = get >>= \(cc, indents) -> - let - -- The last printed character was a line break + goOne x xs = + get >>= \(cc, indents) -> + let -- The last printed character was a line break needsIndent = (cc == 0) putNL :: Int -> State St [Text] putNL n = put (0, indents) $> [newlines n] in case x of - -- Special case trailing comments. Because in cases like - -- [ # comment - -- 1 - -- ] - -- the comment will be parsed as associated to the inner element next time, rendering it as - -- [ - -- # comment - -- 1 - -- ] - -- This breaks idempotency. To work around this, we simply shift the comment by one: - -- [ # comment - -- 1 - -- ] - Text _ _ TrailingComment t | cc == 2 && fst (nextIndent xs) > lineNL -> putText' [" ", t] - where lineNL = snd $ NonEmpty.head indents - Text nl off _ t -> putText nl off t - - -- This code treats whitespace as "expanded" - -- A new line resets the column counter and sets the target indentation as current indentation - Spacing sp - -- We know that the last printed character was a line break (cc == 0), - -- therefore drop any leading whitespace within the group to avoid duplicate newlines - | needsIndent -> pure [] - | otherwise -> case sp of - Break -> putNL 1 - Space -> putNL 1 - Hardspace -> putText' [" "] - Hardline -> putNL 1 - Emptyline -> putNL 2 - (Newlines n) -> putNL n - Softbreak - | firstLineFits (tw - cc) tw xs - -> pure [] - | otherwise -> putNL 1 - Softspace - | firstLineFits (tw - cc - 1) tw xs - -> putText' [" "] - | otherwise -> putNL 1 - - Group ann ys -> - let - -- fromMaybe lifted to (StateT s Maybe) - fromMaybeState :: State s a -> StateT s Maybe a -> State s a - fromMaybeState l r = state $ \s -> fromMaybe (runState l s) (runStateT r s) - in - -- Try to fit the entire group first - goGroup ys xs - -- If that fails, check whether the group contains any priority groups within its children and try to expand them first - -- Ignore transparent groups as their priority children have already been handled up in the parent (and failed) - <|> (if ann /= Transparent then - -- Each priority group will be handled individually, and the priority groups are tried in reverse order - asum $ map (`goPriorityGroup` xs) $ reverse $ priorityGroups ys - else - empty - ) - -- Otherwise, dissolve the group by mapping its members to the target indentation - -- This also implies that whitespace in there will now be rendered "expanded". - & fromMaybeState (go ys xs) + -- Special case trailing comments. Because in cases like + -- [ # comment + -- 1 + -- ] + -- the comment will be parsed as associated to the inner element next time, rendering it as + -- [ + -- # comment + -- 1 + -- ] + -- This breaks idempotency. To work around this, we simply shift the comment by one: + -- [ # comment + -- 1 + -- ] + Text _ _ TrailingComment t | cc == 2 && fst (nextIndent xs) > lineNL -> putText' [" ", t] + where + lineNL = snd $ NonEmpty.head indents + Text nl off _ t -> putText nl off t + -- This code treats whitespace as "expanded" + -- A new line resets the column counter and sets the target indentation as current indentation + Spacing sp + -- We know that the last printed character was a line break (cc == 0), + -- therefore drop any leading whitespace within the group to avoid duplicate newlines + | needsIndent -> pure [] + | otherwise -> case sp of + Break -> putNL 1 + Space -> putNL 1 + Hardspace -> putText' [" "] + Hardline -> putNL 1 + Emptyline -> putNL 2 + (Newlines n) -> putNL n + Softbreak + | firstLineFits (tw - cc) tw xs -> + pure [] + | otherwise -> putNL 1 + Softspace + | firstLineFits (tw - cc - 1) tw xs -> + putText' [" "] + | otherwise -> putNL 1 + Group ann ys -> + let -- fromMaybe lifted to (StateT s Maybe) + fromMaybeState :: State s a -> StateT s Maybe a -> State s a + fromMaybeState l r = state $ \s -> fromMaybe (runState l s) (runStateT r s) + in -- Try to fit the entire group first + goGroup ys xs + -- If that fails, check whether the group contains any priority groups within its children and try to expand them first + -- Ignore transparent groups as their priority children have already been handled up in the parent (and failed) + <|> ( if ann /= Transparent + then -- Each priority group will be handled individually, and the priority groups are tried in reverse order + asum $ map (`goPriorityGroup` xs) $ reverse $ priorityGroups ys + else empty + ) + -- Otherwise, dissolve the group by mapping its members to the target indentation + -- This also implies that whitespace in there will now be rendered "expanded". + & fromMaybeState (go ys xs) goPriorityGroup :: (Doc, Doc, Doc) -> Doc -> StateT St Maybe [Text] goPriorityGroup (pre, prio, post) rest = do - -- Try to fit pre onto one line - preRendered <- goGroup pre (prio ++ post ++ rest) - -- Render prio expanded - -- We know that post will be rendered compact. So we tell the renderer that by manually removing all - -- line breaks in post here. Otherwise we might get into awkward the situation where pre and prio are put - -- onto the one line, all three obviously wouldn't fit. - prioRendered <- mapStateT (Just . runIdentity) $ - go prio (unexpandSpacing post ++ rest) - -- Try to render post onto one line - postRendered <- goGroup post rest - -- If none of these failed, put together and return - return (preRendered ++ prioRendered ++ postRendered) + -- Try to fit pre onto one line + preRendered <- goGroup pre (prio ++ post ++ rest) + -- Render prio expanded + -- We know that post will be rendered compact. So we tell the renderer that by manually removing all + -- line breaks in post here. Otherwise we might get into awkward the situation where pre and prio are put + -- onto the one line, all three obviously wouldn't fit. + prioRendered <- + mapStateT (Just . runIdentity) $ + go prio (unexpandSpacing post ++ rest) + -- Try to render post onto one line + postRendered <- goGroup post rest + -- If none of these failed, put together and return + return (preRendered ++ prioRendered ++ postRendered) -- Try to fit the group onto a single line, while accounting for the fact that the first -- bits of rest must fit as well (until the first possibility for a line break within rest). @@ -604,27 +601,25 @@ layoutGreedy tw doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, s -- gets called for pre and post of priority groups, which may be empty. goGroup [] _ = pure [] goGroup grp rest = StateT $ \(cc, ci) -> - if cc == 0 then - let - -- We know that the last printed character was a line break (cc == 0), - -- therefore drop any leading whitespace within the group to avoid duplicate newlines - grp' = case head grp of - Spacing _ -> tail grp - Group ann ((Spacing _) : inner) -> Group ann inner : tail grp - _ -> grp - (nl, off) = nextIndent grp' - - indentWillIncrease = if fst (nextIndent rest) > lineNL then 2 else 0 - where - lastLineNL = snd $ NonEmpty.head ci - lineNL = lastLineNL + (if nl > lastLineNL then 2 else 0) - in - fits indentWillIncrease (tw - firstLineWidth rest) grp' - <&> \t -> runState (putText nl off t) (cc, ci) + if cc == 0 + then + let -- We know that the last printed character was a line break (cc == 0), + -- therefore drop any leading whitespace within the group to avoid duplicate newlines + grp' = case head grp of + Spacing _ -> tail grp + Group ann ((Spacing _) : inner) -> Group ann inner : tail grp + _ -> grp + (nl, off) = nextIndent grp' + + indentWillIncrease = if fst (nextIndent rest) > lineNL then 2 else 0 + where + lastLineNL = snd $ NonEmpty.head ci + lineNL = lastLineNL + (if nl > lastLineNL then 2 else 0) + in fits indentWillIncrease (tw - firstLineWidth rest) grp' + <&> \t -> runState (putText nl off t) (cc, ci) else - let - indentWillIncrease = if fst (nextIndent rest) > lineNL then 2 else 0 - where lineNL = snd $ NonEmpty.head ci - in - fits (indentWillIncrease - cc) (tw - cc - firstLineWidth rest) grp - <&> \t -> ([t], (cc + textWidth t, ci)) + let indentWillIncrease = if fst (nextIndent rest) > lineNL then 2 else 0 + where + lineNL = snd $ NonEmpty.head ci + in fits (indentWillIncrease - cc) (tw - cc - firstLineWidth rest) grp + <&> \t -> ([t], (cc + textWidth t, ci)) diff --git a/src/Nixfmt/Pretty.hs b/src/Nixfmt/Pretty.hs index 455d57bb..2ffdfd6f 100644 --- a/src/Nixfmt/Pretty.hs +++ b/src/Nixfmt/Pretty.hs @@ -1,23 +1,66 @@ -{-# LANGUAGE FlexibleInstances, OverloadedStrings, RankNTypes, TupleSections, LambdaCase #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TupleSections #-} module Nixfmt.Pretty where -import Prelude hiding (String) - import Data.Char (isSpace) -import Data.Maybe (fromMaybe, isJust, isNothing, fromJust, maybeToList) +import Data.Maybe (fromJust, fromMaybe, isJust, isNothing, maybeToList) import Data.Text (Text) import qualified Data.Text as Text (null, takeWhile) - -import Nixfmt.Predoc - (Doc, GroupAnn(..), Pretty, emptyline, group, group', hardline, hardspace, hcat, line, line', - nest, offset, newline, pretty, sepBy, surroundWith, softline, softline', text, comment, trailingComment, trailing, textWidth, - unexpandSpacing') -import Nixfmt.Types - (Ann(..), Binder(..), Expression(..), Item(..), Items(..), Leaf, - ParamAttr(..), Parameter(..), Selector(..), SimpleSelector(..), - StringPart(..), Term(..), Token(..), TrailingComment(..), Trivium(..), - Whole(..), tokenText, mapFirstToken, mapFirstToken', mapLastToken', hasTrivia) +import Nixfmt.Predoc ( + Doc, + GroupAnn (..), + Pretty, + comment, + emptyline, + group, + group', + hardline, + hardspace, + hcat, + line, + line', + nest, + newline, + offset, + pretty, + sepBy, + softline, + softline', + surroundWith, + text, + textWidth, + trailing, + trailingComment, + unexpandSpacing', + ) +import Nixfmt.Types ( + Ann (..), + Binder (..), + Expression (..), + Item (..), + Items (..), + Leaf, + ParamAttr (..), + Parameter (..), + Selector (..), + SimpleSelector (..), + StringPart (..), + Term (..), + Token (..), + TrailingComment (..), + Trivium (..), + Whole (..), + hasTrivia, + mapFirstToken, + mapFirstToken', + mapLastToken', + tokenText, + ) +import Prelude hiding (String) toLineComment :: TrailingComment -> Trivium toLineComment (TrailingComment c) = LineComment $ " " <> c @@ -28,82 +71,91 @@ moveTrailingCommentUp (Ann pre a (Just post)) = Ann (pre ++ [toLineComment post] moveTrailingCommentUp a = a instance Pretty TrailingComment where - pretty (TrailingComment c) - = hardspace <> trailingComment ("# " <> c) <> hardline + pretty (TrailingComment c) = + hardspace <> trailingComment ("# " <> c) <> hardline instance Pretty Trivium where - pretty EmptyLine = emptyline - pretty (LineComment c) = comment ("#" <> c) <> hardline - pretty (BlockComment isDoc c) = - comment (if isDoc then "/**" else "/*") <> hardline - -- Indent the comment using offset instead of nest - <> offset 2 (hcat $ map prettyCommentLine c) - <> comment "*/" <> hardline - where - prettyCommentLine :: Text -> Doc - prettyCommentLine l - | Text.null l = emptyline - | otherwise = comment l <> hardline - - -instance Pretty a => Pretty (Item a) where - pretty (DetachedComments trivia) = pretty trivia - pretty (CommentedItem trivia x) = pretty trivia <> group x + pretty EmptyLine = emptyline + pretty (LineComment c) = comment ("#" <> c) <> hardline + pretty (BlockComment isDoc c) = + comment (if isDoc then "/**" else "/*") + <> hardline + -- Indent the comment using offset instead of nest + <> offset 2 (hcat $ map prettyCommentLine c) + <> comment "*/" + <> hardline + where + prettyCommentLine :: Text -> Doc + prettyCommentLine l + | Text.null l = emptyline + | otherwise = comment l <> hardline + +instance (Pretty a) => Pretty (Item a) where + pretty (DetachedComments trivia) = pretty trivia + pretty (CommentedItem trivia x) = pretty trivia <> group x -- For lists, attribute sets and let bindings -prettyItems :: Pretty a => Doc -> Items a -> Doc +prettyItems :: (Pretty a) => Doc -> Items a -> Doc -- Special case: Preserve an empty line with no items -- usually, trailing newlines after the last element are not preserved prettyItems _ (Items [DetachedComments []]) = emptyline prettyItems sep items = prettyItems' $ unItems items where - prettyItems' :: Pretty a => [Item a] -> Doc + prettyItems' :: (Pretty a) => [Item a] -> Doc prettyItems' [] = mempty prettyItems' [item] = pretty item - prettyItems' (item : xs) - = pretty item - <> case item of { CommentedItem _ _ -> sep; DetachedComments _ -> emptyline } + prettyItems' (item : xs) = + pretty item + <> case item of CommentedItem _ _ -> sep; DetachedComments _ -> emptyline <> prettyItems' xs instance Pretty [Trivium] where - pretty [] = mempty - pretty trivia = hardline <> hcat trivia + pretty [] = mempty + pretty trivia = hardline <> hcat trivia -instance Pretty a => Pretty (Ann a) where - pretty (Ann leading x trailing') - = pretty leading <> pretty x <> pretty trailing' +instance (Pretty a) => Pretty (Ann a) where + pretty (Ann leading x trailing') = + pretty leading <> pretty x <> pretty trailing' instance Pretty SimpleSelector where - pretty (IDSelector i) = pretty i - pretty (InterpolSelector interpol) = pretty interpol - pretty (StringSelector (Ann leading s trailing')) - = pretty leading <> prettySimpleString s <> pretty trailing' + pretty (IDSelector i) = pretty i + pretty (InterpolSelector interpol) = pretty interpol + pretty (StringSelector (Ann leading s trailing')) = + pretty leading <> prettySimpleString s <> pretty trailing' instance Pretty Selector where - pretty (Selector dot sel) - = pretty dot <> pretty sel + pretty (Selector dot sel) = + pretty dot <> pretty sel instance Pretty Binder where - -- `inherit bar` statement - pretty (Inherit inherit Nothing ids semicolon) - = group $ pretty inherit - <> (if null ids then pretty semicolon else - line <> nest (sepBy (if length ids < 4 then line else hardline) ids <> line' <> pretty semicolon) - ) - - -- `inherit (foo) bar` statement - pretty (Inherit inherit (Just source) ids semicolon) - = group $ pretty inherit <> nest ( - group' RegularG (line <> pretty source) - <> if null ids then pretty semicolon else line - <> sepBy (if length ids < 4 then line else hardline) ids - <> line' <> pretty semicolon - ) - - -- `foo = bar` - pretty (Assignment selectors assign expr semicolon) - = group $ hcat selectors - <> nest (hardspace <> pretty assign <> nest (absorbRHS expr)) <> pretty semicolon + -- `inherit bar` statement + pretty (Inherit inherit Nothing ids semicolon) = + group $ + pretty inherit + <> ( if null ids + then pretty semicolon + else line <> nest (sepBy (if length ids < 4 then line else hardline) ids <> line' <> pretty semicolon) + ) + -- `inherit (foo) bar` statement + pretty (Inherit inherit (Just source) ids semicolon) = + group $ + pretty inherit + <> nest + ( group' RegularG (line <> pretty source) + <> if null ids + then pretty semicolon + else + line + <> sepBy (if length ids < 4 then line else hardline) ids + <> line' + <> pretty semicolon + ) + -- `foo = bar` + pretty (Assignment selectors assign expr semicolon) = + group $ + hcat selectors + <> nest (hardspace <> pretty assign <> nest (absorbRHS expr)) + <> pretty semicolon -- Pretty a set -- while we already pretty eagerly expand sets with more than one element, @@ -111,17 +163,17 @@ instance Pretty Binder where -- be even more eager at expanding, except for empty sets and inherit statements. prettySet :: Bool -> (Maybe Leaf, Leaf, Items Binder, Leaf) -> Doc -- Empty attribute set -prettySet _ (krec, Ann [] paropen Nothing, Items [], parclose@(Ann [] _ _)) - = pretty (fmap (, hardspace) krec) <> pretty paropen <> hardspace <> pretty parclose +prettySet _ (krec, Ann [] paropen Nothing, Items [], parclose@(Ann [] _ _)) = + pretty (fmap (,hardspace) krec) <> pretty paropen <> hardspace <> pretty parclose -- Singleton sets are allowed to fit onto one line, -- but apart from that always expand. -prettySet wide (krec, Ann pre paropen post, binders, parclose) - = pretty (fmap (, hardspace) krec) <> - pretty (Ann pre paropen Nothing) - <> surroundWith sep (nest $ pretty post <> prettyItems hardline binders) - <> pretty parclose - where - sep = if wide && not (null (unItems binders)) then hardline else line +prettySet wide (krec, Ann pre paropen post, binders, parclose) = + pretty (fmap (,hardspace) krec) + <> pretty (Ann pre paropen Nothing) + <> surroundWith sep (nest $ pretty post <> prettyItems hardline binders) + <> pretty parclose + where + sep = if wide && not (null (unItems binders)) then hardline else line prettyTermWide :: Term -> Doc prettyTermWide (Set krec paropen items parclose) = prettySet True (krec, paropen, items, parclose) @@ -134,37 +186,36 @@ prettyTerm (SimpleString (Ann leading s trailing')) = pretty leading <> prettySi prettyTerm (IndentedString (Ann leading s trailing')) = pretty leading <> prettyIndentedString s <> pretty trailing' prettyTerm (Path p) = pretty p prettyTerm (Selection term selectors rest) = - pretty term <> sep <> hcat selectors + pretty term + <> sep + <> hcat selectors <> pretty ((\(kw, def) -> softline <> nest (pretty kw <> hardspace <> pretty def)) <$> rest) - where - -- Selection (`foo.bar.baz`) case distinction on the first element (`foo`): - sep = case term of - -- If it is an ident, keep it all together - (Token _) -> mempty - -- If it is a parenthesized expression, maybe add a line break - (Parenthesized {}) -> softline' - -- Otherwise, very likely add a line break - _ -> line' + where + -- Selection (`foo.bar.baz`) case distinction on the first element (`foo`): + sep = case term of + -- If it is an ident, keep it all together + (Token _) -> mempty + -- If it is a parenthesized expression, maybe add a line break + (Parenthesized{}) -> softline' + -- Otherwise, very likely add a line break + _ -> line' -- Empty list -prettyTerm (List (Ann leading paropen Nothing) (Items []) (Ann [] parclose trailing')) - = pretty leading <> pretty paropen <> hardspace <> pretty parclose <> pretty trailing' - +prettyTerm (List (Ann leading paropen Nothing) (Items []) (Ann [] parclose trailing')) = + pretty leading <> pretty paropen <> hardspace <> pretty parclose <> pretty trailing' -- General list -- Always expand if len > 1 prettyTerm (List (Ann pre paropen post) items parclose) = - pretty (Ann pre paropen Nothing) + pretty (Ann pre paropen Nothing) <> surroundWith line (nest $ pretty post <> prettyItems hardline items) <> pretty parclose - prettyTerm (Set krec paropen items parclose) = prettySet False (krec, paropen, items, parclose) - -- Parentheses -prettyTerm (Parenthesized paropen expr (Ann closePre parclose closePost)) - = group $ - pretty (moveTrailingCommentUp paropen) - <> nest (inner <> pretty closePre) - <> pretty (Ann [] parclose closePost) +prettyTerm (Parenthesized paropen expr (Ann closePre parclose closePost)) = + group $ + pretty (moveTrailingCommentUp paropen) + <> nest (inner <> pretty closePre) + <> pretty (Ann [] parclose closePost) where inner = case expr of @@ -174,30 +225,29 @@ prettyTerm (Parenthesized paropen expr (Ann closePre parclose closePost)) (Application f a) -> prettyApp True mempty True f a -- Same thing for selections (Term (Selection t _ _)) | isAbsorbable t -> line' <> group expr <> line' - (Term (Selection {})) -> group expr <> line' + (Term (Selection{})) -> group expr <> line' -- Start on a new line for the others _ -> line' <> group expr <> line' instance Pretty Term where - pretty l@List{} = group $ prettyTerm l - pretty x = prettyTerm x + pretty l@List{} = group $ prettyTerm l + pretty x = prettyTerm x -- Does not move around comments, nor does it inject a trailing comma instance Pretty ParamAttr where - -- Simple parameter (no default) - pretty (ParamAttr name Nothing maybeComma) - = pretty name <> pretty maybeComma - - -- With ? default - pretty (ParamAttr name (Just (qmark, def)) maybeComma) - = group $ - pretty name <> hardspace - <> nest (pretty qmark <> nest (absorbRHS def)) - <> pretty maybeComma - - -- `...` - pretty (ParamEllipsis ellipsis) - = pretty ellipsis + -- Simple parameter (no default) + pretty (ParamAttr name Nothing maybeComma) = + pretty name <> pretty maybeComma + -- With ? default + pretty (ParamAttr name (Just (qmark, def)) maybeComma) = + group $ + pretty name + <> hardspace + <> nest (pretty qmark <> nest (absorbRHS def)) + <> pretty maybeComma + -- `...` + pretty (ParamEllipsis ellipsis) = + pretty ellipsis -- Move comments around when switching from leading comma to trailing comma style: -- `, name # foo` → `name, #foo` @@ -205,17 +255,20 @@ instance Pretty ParamAttr where -- This assumes that all items already have a trailing comma from earlier pre-processing moveParamAttrComment :: ParamAttr -> ParamAttr -- Simple parameter -moveParamAttrComment (ParamAttr (Ann trivia name (Just comment')) Nothing (Just (Ann [] comma Nothing))) - = ParamAttr (Ann trivia name Nothing) Nothing (Just (Ann [] comma (Just comment'))) +moveParamAttrComment (ParamAttr (Ann trivia name (Just comment')) Nothing (Just (Ann [] comma Nothing))) = + ParamAttr (Ann trivia name Nothing) Nothing (Just (Ann [] comma (Just comment'))) -- Parameter with default value -moveParamAttrComment (ParamAttr name (Just (qmark, def)) (Just (Ann [] comma Nothing))) - = ParamAttr name (Just (qmark, def')) (Just (Ann [] comma comment')) - where - -- Extract comment at the end of the line - (def', comment') = mapLastToken' (\case +moveParamAttrComment (ParamAttr name (Just (qmark, def)) (Just (Ann [] comma Nothing))) = + ParamAttr name (Just (qmark, def')) (Just (Ann [] comma comment')) + where + -- Extract comment at the end of the line + (def', comment') = + mapLastToken' + ( \case (Ann trivia t (Just comment'')) -> (Ann trivia t Nothing, Just comment'') ann -> (ann, Nothing) - ) def + ) + def moveParamAttrComment x = x -- When a `, name` entry has some line comments before it, they are actually attached to the comment @@ -223,64 +276,63 @@ moveParamAttrComment x = x -- Also adds the trailing comma on the last element if necessary moveParamsComments :: [ParamAttr] -> [ParamAttr] moveParamsComments - -- , name1 - -- # comment - -- , name2 - ((ParamAttr name maybeDefault (Just (Ann trivia comma Nothing))) : - (ParamAttr (Ann trivia' name' Nothing) maybeDefault' maybeComma') : - xs) - = ParamAttr name maybeDefault (Just (Ann [] comma Nothing)) - : moveParamsComments (ParamAttr (Ann (trivia ++ trivia') name' Nothing) maybeDefault' maybeComma' : xs) + -- , name1 + -- # comment + -- , name2 + ( (ParamAttr name maybeDefault (Just (Ann trivia comma Nothing))) + : (ParamAttr (Ann trivia' name' Nothing) maybeDefault' maybeComma') + : xs + ) = + ParamAttr name maybeDefault (Just (Ann [] comma Nothing)) + : moveParamsComments (ParamAttr (Ann (trivia ++ trivia') name' Nothing) maybeDefault' maybeComma' : xs) -- This may seem like a nonsensical case, but keep in mind that blank lines also count as comments (trivia) moveParamsComments - -- , name - -- # comment - -- ellipsis - [ParamAttr name maybeDefault (Just (Ann trivia comma Nothing)) - ,ParamEllipsis (Ann trivia' name' trailing')] - = [ParamAttr name maybeDefault (Just (Ann [] comma Nothing)) - , ParamEllipsis (Ann (trivia ++ trivia') name' trailing')] + -- , name + -- # comment + -- ellipsis + [ ParamAttr name maybeDefault (Just (Ann trivia comma Nothing)), + ParamEllipsis (Ann trivia' name' trailing') + ] = + [ ParamAttr name maybeDefault (Just (Ann [] comma Nothing)), + ParamEllipsis (Ann (trivia ++ trivia') name' trailing') + ] -- Inject a trailing comma on the last element if nessecary moveParamsComments [ParamAttr name def Nothing] = [ParamAttr name def (Just (Ann [] TComma Nothing))] moveParamsComments (x : xs) = x : moveParamsComments xs moveParamsComments [] = [] instance Pretty Parameter where - -- param: - pretty (IDParameter i) = pretty i - - -- {}: - pretty (SetParameter bopen [] bclose) - = group $ pretty (moveTrailingCommentUp bopen) <> hardspace <> pretty bclose - - -- { stuff }: - pretty (SetParameter bopen attrs bclose) = - group $ - pretty (moveTrailingCommentUp bopen) - <> surroundWith sep (nest $ sepBy sep $ handleTrailingComma $ map moveParamAttrComment $ moveParamsComments attrs) - <> pretty bclose - where - -- pretty all ParamAttrs, but mark the trailing comma of the last element specially - -- This is so that the trailing comma will only be printed in the expanded form - handleTrailingComma :: [ParamAttr] -> [Doc] - handleTrailingComma [] = [] - -- That's the case we're interested in - handleTrailingComma [ParamAttr name maybeDefault (Just (Ann [] TComma Nothing))] - = [pretty (ParamAttr name maybeDefault Nothing) <> trailing ","] - handleTrailingComma (x:xs) = pretty x : handleTrailingComma xs - - sep = case attrs of - [ParamEllipsis _] -> line - -- Attributes must be without default - [ParamAttr _ Nothing _] -> line - [ParamAttr _ Nothing _, ParamEllipsis _] -> line - [ParamAttr _ Nothing _, ParamAttr _ Nothing _] -> line - [ParamAttr _ Nothing _, ParamAttr _ Nothing _, ParamEllipsis _] -> line - _ -> hardline - - pretty (ContextParameter param1 at param2) - = pretty param1 <> pretty at <> pretty param2 - + -- param: + pretty (IDParameter i) = pretty i + -- {}: + pretty (SetParameter bopen [] bclose) = + group $ pretty (moveTrailingCommentUp bopen) <> hardspace <> pretty bclose + -- { stuff }: + pretty (SetParameter bopen attrs bclose) = + group $ + pretty (moveTrailingCommentUp bopen) + <> surroundWith sep (nest $ sepBy sep $ handleTrailingComma $ map moveParamAttrComment $ moveParamsComments attrs) + <> pretty bclose + where + -- pretty all ParamAttrs, but mark the trailing comma of the last element specially + -- This is so that the trailing comma will only be printed in the expanded form + handleTrailingComma :: [ParamAttr] -> [Doc] + handleTrailingComma [] = [] + -- That's the case we're interested in + handleTrailingComma [ParamAttr name maybeDefault (Just (Ann [] TComma Nothing))] = + [pretty (ParamAttr name maybeDefault Nothing) <> trailing ","] + handleTrailingComma (x : xs) = pretty x : handleTrailingComma xs + + sep = case attrs of + [ParamEllipsis _] -> line + -- Attributes must be without default + [ParamAttr _ Nothing _] -> line + [ParamAttr _ Nothing _, ParamEllipsis _] -> line + [ParamAttr _ Nothing _, ParamAttr _ Nothing _] -> line + [ParamAttr _ Nothing _, ParamAttr _ Nothing _, ParamEllipsis _] -> line + _ -> hardline + pretty (ContextParameter param1 at param2) = + pretty param1 <> pretty at <> pretty param2 -- Function application -- Some example mapping of Nix code to Doc (using brackets as groups, but omitting the outermost group @@ -305,108 +357,135 @@ instance Pretty Parameter where -- This means that callers can say "try to be compact first, but if more than the last argument does not fit onto the line, -- then start on a new line instead". prettyApp :: Bool -> Doc -> Bool -> Expression -> Expression -> Doc -prettyApp indentFunction pre hasPost f a - = let - absorbApp (Application f' a') = group' Transparent (absorbApp f') <> line <> nest (group' Priority a') - absorbApp expr - | indentFunction && null comment' = nest $ group' RegularG $ line' <> pretty expr - | otherwise = pretty expr - - absorbLast (Term t) | isAbsorbable t - = group' Priority $ nest $ prettyTerm t - -- Special case: Absorb parenthesized function declaration with absorbable body - absorbLast - (Term (Parenthesized - open (Abstraction (IDParameter name) colon (Term body)) close - )) - | isAbsorbableTerm body && not (any hasTrivia [open, name, colon]) - = group' Priority $ nest $ - pretty open <> pretty name <> pretty colon <> hardspace - <> prettyTermWide body - <> pretty close - -- Special case: Absorb parenthesized function application with absorbable body - absorbLast - (Term (Parenthesized - open (Application (Term (Token ident@(Ann _ fn@(Identifier _) _))) (Term body)) close - )) - | isAbsorbableTerm body && not (any hasTrivia [open, ident, close]) - = group' Priority $ nest $ - pretty open <> pretty fn <> hardspace - <> prettyTermWide body - <> pretty close - absorbLast (Term (Parenthesized open expr close)) - = absorbParen open expr close - absorbLast arg = group' RegularG $ nest $ pretty arg - - -- Extract comment before the first function and move it out, to prevent functions being force-expanded - (fWithoutComment, comment') = mapFirstToken' - ((\(Ann leading token trailing') -> (Ann [] token trailing', leading)) . moveTrailingCommentUp) - f - - renderedF = pre <> group' Transparent (absorbApp fWithoutComment) - renderedFUnexpanded = unexpandSpacing' Nothing renderedF - - post = if hasPost then line' else mempty - in - pretty comment' - <> ( - if isSimple (Application f a) && isJust renderedFUnexpanded then - group' RegularG $ fromJust renderedFUnexpanded <> hardspace <> absorbLast a - else - group' RegularG $ renderedF <> line <> absorbLast a <> post - ) - <> (if hasPost && not (null comment') then hardline else mempty) +prettyApp indentFunction pre hasPost f a = + let absorbApp (Application f' a') = group' Transparent (absorbApp f') <> line <> nest (group' Priority a') + absorbApp expr + | indentFunction && null comment' = nest $ group' RegularG $ line' <> pretty expr + | otherwise = pretty expr + + absorbLast (Term t) + | isAbsorbable t = + group' Priority $ nest $ prettyTerm t + -- Special case: Absorb parenthesized function declaration with absorbable body + absorbLast + ( Term + ( Parenthesized + open + (Abstraction (IDParameter name) colon (Term body)) + close + ) + ) + | isAbsorbableTerm body && not (any hasTrivia [open, name, colon]) = + group' Priority $ + nest $ + pretty open + <> pretty name + <> pretty colon + <> hardspace + <> prettyTermWide body + <> pretty close + -- Special case: Absorb parenthesized function application with absorbable body + absorbLast + ( Term + ( Parenthesized + open + (Application (Term (Token ident@(Ann _ fn@(Identifier _) _))) (Term body)) + close + ) + ) + | isAbsorbableTerm body && not (any hasTrivia [open, ident, close]) = + group' Priority $ + nest $ + pretty open + <> pretty fn + <> hardspace + <> prettyTermWide body + <> pretty close + absorbLast (Term (Parenthesized open expr close)) = + absorbParen open expr close + absorbLast arg = group' RegularG $ nest $ pretty arg + + -- Extract comment before the first function and move it out, to prevent functions being force-expanded + (fWithoutComment, comment') = + mapFirstToken' + ((\(Ann leading token trailing') -> (Ann [] token trailing', leading)) . moveTrailingCommentUp) + f + + renderedF = pre <> group' Transparent (absorbApp fWithoutComment) + renderedFUnexpanded = unexpandSpacing' Nothing renderedF + + post = if hasPost then line' else mempty + in pretty comment' + <> ( if isSimple (Application f a) && isJust renderedFUnexpanded + then group' RegularG $ fromJust renderedFUnexpanded <> hardspace <> absorbLast a + else group' RegularG $ renderedF <> line <> absorbLast a <> post + ) + <> (if hasPost && not (null comment') then hardline else mempty) prettyWith :: Bool -> Expression -> Doc -- absorb the body -prettyWith True (With with expr0 semicolon (Term expr1)) - = group' RegularG $ - line' <> - pretty with <> hardspace - <> nest (group expr0) <> pretty semicolon - -- Force-expand attrsets - <> hardspace <> group' Priority (prettyTermWide expr1) +prettyWith True (With with expr0 semicolon (Term expr1)) = + group' RegularG $ + line' + <> pretty with + <> hardspace + <> nest (group expr0) + <> pretty semicolon + -- Force-expand attrsets + <> hardspace + <> group' Priority (prettyTermWide expr1) -- Normal case -prettyWith _ (With with expr0 semicolon expr1) - = group ( - pretty with <> hardspace - <> nest (group expr0) <> pretty semicolon - ) - <> line <> pretty expr1 +prettyWith _ (With with expr0 semicolon expr1) = + group + ( pretty with + <> hardspace + <> nest (group expr0) + <> pretty semicolon + ) + <> line + <> pretty expr1 prettyWith _ _ = error "unreachable" isAbsorbableExpr :: Expression -> Bool isAbsorbableExpr expr = case expr of - (Term t) | isAbsorbableTerm t -> True - (With _ _ _ (Term t)) | isAbsorbableTerm t -> True - -- Absorb function declarations but only those with simple parameter(s) - (Abstraction (IDParameter _) _ (Term t)) | isAbsorbable t -> True - (Abstraction (IDParameter _) _ body@(Abstraction {})) -> isAbsorbableExpr body - _ -> False + (Term t) | isAbsorbableTerm t -> True + (With _ _ _ (Term t)) | isAbsorbableTerm t -> True + -- Absorb function declarations but only those with simple parameter(s) + (Abstraction (IDParameter _) _ (Term t)) | isAbsorbable t -> True + (Abstraction (IDParameter _) _ body@(Abstraction{})) -> isAbsorbableExpr body + _ -> False isAbsorbable :: Term -> Bool -- Multi-line indented string -isAbsorbable (IndentedString (Ann _ (_:_:_) _)) = True +isAbsorbable (IndentedString (Ann _ (_ : _ : _) _)) = True isAbsorbable (Path _) = True -- Non-empty sets and lists -isAbsorbable (Set _ _ (Items (_:_)) _) = True -isAbsorbable (List _ (Items (_:_)) _) = True -isAbsorbable (Parenthesized (Ann [] _ Nothing) (Term t) _) = isAbsorbable t -isAbsorbable _ = False +isAbsorbable (Set _ _ (Items (_ : _)) _) = True +isAbsorbable (List _ (Items (_ : _)) _) = True +isAbsorbable (Parenthesized (Ann [] _ Nothing) (Term t) _) = isAbsorbable t +isAbsorbable _ = False isAbsorbableTerm :: Term -> Bool isAbsorbableTerm = isAbsorbable absorbParen :: Ann Token -> Expression -> Ann Token -> Doc -absorbParen (Ann pre' open post') expr (Ann pre'' close post'') - = group' Priority $ nest $ pretty (Ann pre' open Nothing) +absorbParen (Ann pre' open post') expr (Ann pre'' close post'') = + group' Priority $ + nest $ + pretty (Ann pre' open Nothing) -- Move any trailing comments on the opening parenthesis down into the body - <> surroundWith line' (group' RegularG $ nest $ - pretty (mapFirstToken - (\(Ann leading token trailing') -> Ann (maybeToList (toLineComment <$> post') ++ leading) token trailing') - expr) - -- Move any leading comments on the closing parenthesis up into the nest - <> pretty pre'') + <> surroundWith + line' + ( group' RegularG $ + nest $ + pretty + ( mapFirstToken + (\(Ann leading token trailing') -> Ann (maybeToList (toLineComment <$> post') ++ leading) token trailing') + expr + ) + -- Move any leading comments on the closing parenthesis up into the nest + <> pretty pre'' + ) <> pretty (Ann [] close post'') -- Note that unlike for absorbable terms which can be force-absorbed, some expressions @@ -422,193 +501,193 @@ absorbExpr _ expr = pretty expr -- Render the RHS value of an assignment or function parameter default value absorbRHS :: Expression -> Doc absorbRHS expr = case expr of - -- Absorbable expression. Always start on the same line - _ | isAbsorbableExpr expr -> hardspace <> group (absorbExpr True expr) - -- Parenthesized expression. Same thing as the special case for parenthesized last argument in function calls. - (Term (Parenthesized open expr' close)) -> hardspace <> absorbParen open expr' close - -- Not all strings are absorbable, but in this case we always want to keep them attached. - -- Because there's nothing to gain from having them start on a new line. - (Term (SimpleString _)) -> hardspace <> group expr - (Term (IndentedString _)) -> hardspace <> group expr - -- Same for path - (Term (Path _)) -> hardspace <> group expr - -- Non-absorbable term - -- If it is multi-line, force it to start on a new line with indentation - (Term _) -> group' RegularG (line <> pretty expr) - -- Function call - -- Absorb if all arguments except the last fit into the line, start on new line otherwise - (Application f a) -> prettyApp False line False f a - (With {}) -> group' RegularG $ line <> pretty expr - -- Special case `//` and `++` operations to be more compact in some cases - -- Case 1: two arguments, LHS is absorbable term, RHS fits onto the last line - (Operation (Term t) (Ann [] op Nothing) b) | isAbsorbable t && isUpdateOrConcat op -> - group' RegularG $ line <> group' Priority (prettyTermWide t) <> line <> pretty op <> hardspace <> pretty b - -- Case 2a: LHS fits onto first line, RHS is an absorbable term - (Operation l (Ann [] op Nothing) (Term t)) | isAbsorbable t && isUpdateOrConcat op -> - group' RegularG $ line <> pretty l <> line <> group' Transparent (pretty op <> hardspace <> group' Priority (prettyTermWide t)) - -- Case 2b: LHS fits onto first line, RHS is a function application - (Operation l (Ann [] op Nothing) (Application f a)) | isUpdateOrConcat op -> - line <> group l <> line <> prettyApp False (pretty op <> hardspace) False f a - -- Everything else: - -- If it fits on one line, it fits - -- If it fits on one line but with a newline after the `=`, it fits (including semicolon) - -- Otherwise, start on new line, expand fully (including the semicolon) - _ -> line <> group expr + -- Absorbable expression. Always start on the same line + _ | isAbsorbableExpr expr -> hardspace <> group (absorbExpr True expr) + -- Parenthesized expression. Same thing as the special case for parenthesized last argument in function calls. + (Term (Parenthesized open expr' close)) -> hardspace <> absorbParen open expr' close + -- Not all strings are absorbable, but in this case we always want to keep them attached. + -- Because there's nothing to gain from having them start on a new line. + (Term (SimpleString _)) -> hardspace <> group expr + (Term (IndentedString _)) -> hardspace <> group expr + -- Same for path + (Term (Path _)) -> hardspace <> group expr + -- Non-absorbable term + -- If it is multi-line, force it to start on a new line with indentation + (Term _) -> group' RegularG (line <> pretty expr) + -- Function call + -- Absorb if all arguments except the last fit into the line, start on new line otherwise + (Application f a) -> prettyApp False line False f a + (With{}) -> group' RegularG $ line <> pretty expr + -- Special case `//` and `++` operations to be more compact in some cases + -- Case 1: two arguments, LHS is absorbable term, RHS fits onto the last line + (Operation (Term t) (Ann [] op Nothing) b) + | isAbsorbable t && isUpdateOrConcat op -> + group' RegularG $ line <> group' Priority (prettyTermWide t) <> line <> pretty op <> hardspace <> pretty b + -- Case 2a: LHS fits onto first line, RHS is an absorbable term + (Operation l (Ann [] op Nothing) (Term t)) + | isAbsorbable t && isUpdateOrConcat op -> + group' RegularG $ line <> pretty l <> line <> group' Transparent (pretty op <> hardspace <> group' Priority (prettyTermWide t)) + -- Case 2b: LHS fits onto first line, RHS is a function application + (Operation l (Ann [] op Nothing) (Application f a)) + | isUpdateOrConcat op -> + line <> group l <> line <> prettyApp False (pretty op <> hardspace) False f a + -- Everything else: + -- If it fits on one line, it fits + -- If it fits on one line but with a newline after the `=`, it fits (including semicolon) + -- Otherwise, start on new line, expand fully (including the semicolon) + _ -> line <> group expr + where + isUpdateOrConcat TUpdate = True + isUpdateOrConcat TConcat = True + isUpdateOrConcat _ = False +instance Pretty Expression where + pretty (Term t) = pretty t + pretty with@(With{}) = prettyWith False with + -- Let bindings are always fully expanded (no single-line form) + -- We also take the comments around the `in` (trailing, leading and detached binder comments) + -- and move them down to the first token of the body + pretty (Let let_ binders (Ann leading in_ trailing') expr) = + letPart <> hardline <> inPart where - isUpdateOrConcat TUpdate = True - isUpdateOrConcat TConcat = True - isUpdateOrConcat _ = False + -- Convert the TrailingComment to a Trivium, if present + convertTrailing Nothing = [] + convertTrailing (Just (TrailingComment t)) = [LineComment (" " <> t)] + + -- Extract detached comments at the bottom. + -- This uses a custom variant of span/spanJust/spanMaybe. + -- Note that this is a foldr which walks from the bottom, but the lists + -- are constructed in a way that they end up correct again. + (binderComments, bindersWithoutComments) = + foldr + ( \item (start, rest) -> case item of + (DetachedComments inner) | null rest -> (inner : start, rest) + _ -> (start, item : rest) + ) + ([], []) + (unItems binders) -instance Pretty Expression where - pretty (Term t) = pretty t - - pretty with@(With {}) = prettyWith False with - - -- Let bindings are always fully expanded (no single-line form) - -- We also take the comments around the `in` (trailing, leading and detached binder comments) - -- and move them down to the first token of the body - pretty (Let let_ binders (Ann leading in_ trailing') expr) - = letPart <> hardline <> inPart - where - -- Convert the TrailingComment to a Trivium, if present - convertTrailing Nothing = [] - convertTrailing (Just (TrailingComment t)) = [LineComment (" " <> t)] - - -- Extract detached comments at the bottom. - -- This uses a custom variant of span/spanJust/spanMaybe. - -- Note that this is a foldr which walks from the bottom, but the lists - -- are constructed in a way that they end up correct again. - (binderComments, bindersWithoutComments) - = foldr - (\ item (start, rest) -> case item of - (DetachedComments inner) | null rest -> (inner : start, rest) - _ -> (start, item : rest) - ) - ([], []) - (unItems binders) - - letPart = group $ pretty let_ <> hardline <> letBody - letBody = nest $ prettyItems hardline (Items bindersWithoutComments) - inPart = group $ pretty (Ann [] in_ Nothing) <> hardline - -- Take our trailing and inject it between `in` and body - <> pretty (concat binderComments ++ leading ++ convertTrailing trailing') - <> pretty expr - - pretty (Assert assert cond semicolon expr) - = group $ - -- Render the assert as if it is was just a function (literally) - uncurry (prettyApp False mempty False) (insertIntoApp (Term $ Token assert) cond) - <> pretty semicolon <> hardline <> pretty expr - where - -- Add something to the left of a function application - -- We need to walk down the arguments here because applications are left-associative. - insertIntoApp :: Expression -> Expression -> (Expression, Expression) - insertIntoApp insert (Application f a) = (uncurry Application $ insertIntoApp insert f, a) - insertIntoApp insert other = (insert, other) - - pretty expr@(If {}) - -- If the first `if` or any `else` has a trailing comment, move it up. - -- However, don't any subsequent `if` (`else if`). We could do that, but that - -- would require taking care of edge cases which are not worth handling. - = group' RegularG $ prettyIf line $ mapFirstToken moveTrailingCommentUp expr - where - -- Recurse to absorb nested "else if" chains - prettyIf :: Doc -> Expression -> Doc - prettyIf sep (If if_ cond then_ expr0 else_ expr1) - -- `if cond then` if it fits on one line, otherwise `if\n cond\nthen` (with cond indented) - = group (pretty if_ <> line <> nest (pretty cond) <> line <> pretty then_) - <> surroundWith sep (nest $ group expr0) - -- Using hardline here is okay because it will only apply to nested ifs, which should not be inline anyways. - <> pretty (moveTrailingCommentUp else_) <> hardspace <> prettyIf hardline expr1 - prettyIf _ x - = line <> nest (group x) - - -- Simple parameter - pretty (Abstraction (IDParameter param) colon body) - = group' RegularG $ line' <> pretty param <> pretty colon <> absorbAbs 1 body - where - absorbAbs :: Int -> Expression -> Doc - -- If there are multiple ID parameters to that function, treat them all at once - absorbAbs depth (Abstraction (IDParameter param0) colon0 body0) = - hardspace <> pretty param0 <> pretty colon0 <> absorbAbs (depth + 1) body0 - absorbAbs _ expr | isAbsorbableExpr expr = hardspace <> group' Priority (absorbExpr False expr) - -- Force the content onto a new line when it is not absorbable and there are more than two arguments - absorbAbs depth x - = (if depth <= 2 then line else hardline) <> pretty x - - -- Attrset parameter - pretty (Abstraction param colon (Term t)) - | isAbsorbable t - = pretty param <> pretty colon <> line <> group (prettyTermWide t) - pretty (Abstraction param colon body) - = pretty param <> pretty colon <> line <> pretty body - - pretty (Application f a) - = prettyApp False mempty False f a - - -- not chainable binary operators: <, >, <=, >=, ==, != - pretty (Operation a op@(Ann _ op' _) b) - | op' == TLess || op' == TGreater || op' == TLessEqual || op' == TGreaterEqual || op' == TEqual || op' == TUnequal - = pretty a <> softline <> pretty op <> hardspace <> pretty b - -- all other operators - pretty operation@(Operation _ op _) - = let - -- Walk the operation tree and put a list of things on the same level. - -- We still need to keep the operators around because they might have comments attached to them. - -- An operator is put together with its succeeding expression. Only the first operand has none. - flatten :: Maybe Leaf -> Expression -> [(Maybe Leaf, Expression)] - flatten opL (Operation a opR b) | opR == op = flatten opL a ++ flatten (Just opR) b - flatten opL x = [(opL, x)] - - -- Called on every operand except the first one (a.k.a. RHS) - absorbOperation :: Expression -> Doc - absorbOperation (Term t) | isAbsorbable t = hardspace <> pretty t - -- Force nested operations to start on a new line - absorbOperation x@(Operation {}) = group' RegularG $ line <> pretty x - -- Force applications to start on a new line if more than the last argument is multiline - absorbOperation (Application f a) = group $ prettyApp False line False f a - absorbOperation x = hardspace <> pretty x - - prettyOperation :: (Maybe Leaf, Expression) -> Doc - -- First element - prettyOperation (Nothing, expr) = pretty expr - -- The others - prettyOperation (Just op', expr) = - line <> pretty (moveTrailingCommentUp op') <> nest (absorbOperation expr) - in - group' RegularG $ - (concatMap prettyOperation . flatten Nothing) operation - - pretty (MemberCheck expr qmark sel) - = pretty expr <> softline - <> pretty qmark <> hardspace <> hcat sel - - pretty (Negation minus expr) - = pretty minus <> pretty expr - - pretty (Inversion bang expr) - = pretty bang <> pretty expr - -instance Pretty a => Pretty (Whole a) where - pretty (Whole x finalTrivia) - = group $ pretty x <> pretty finalTrivia + letPart = group $ pretty let_ <> hardline <> letBody + letBody = nest $ prettyItems hardline (Items bindersWithoutComments) + inPart = + group $ + pretty (Ann [] in_ Nothing) + <> hardline + -- Take our trailing and inject it between `in` and body + <> pretty (concat binderComments ++ leading ++ convertTrailing trailing') + <> pretty expr + pretty (Assert assert cond semicolon expr) = + group $ + -- Render the assert as if it is was just a function (literally) + uncurry (prettyApp False mempty False) (insertIntoApp (Term $ Token assert) cond) + <> pretty semicolon + <> hardline + <> pretty expr + where + -- Add something to the left of a function application + -- We need to walk down the arguments here because applications are left-associative. + insertIntoApp :: Expression -> Expression -> (Expression, Expression) + insertIntoApp insert (Application f a) = (uncurry Application $ insertIntoApp insert f, a) + insertIntoApp insert other = (insert, other) + pretty expr@(If{}) = + -- If the first `if` or any `else` has a trailing comment, move it up. + -- However, don't any subsequent `if` (`else if`). We could do that, but that + -- would require taking care of edge cases which are not worth handling. + group' RegularG $ prettyIf line $ mapFirstToken moveTrailingCommentUp expr + where + -- Recurse to absorb nested "else if" chains + prettyIf :: Doc -> Expression -> Doc + prettyIf sep (If if_ cond then_ expr0 else_ expr1) = + -- `if cond then` if it fits on one line, otherwise `if\n cond\nthen` (with cond indented) + group (pretty if_ <> line <> nest (pretty cond) <> line <> pretty then_) + <> surroundWith sep (nest $ group expr0) + -- Using hardline here is okay because it will only apply to nested ifs, which should not be inline anyways. + <> pretty (moveTrailingCommentUp else_) + <> hardspace + <> prettyIf hardline expr1 + prettyIf _ x = + line <> nest (group x) + + -- Simple parameter + pretty (Abstraction (IDParameter param) colon body) = + group' RegularG $ line' <> pretty param <> pretty colon <> absorbAbs 1 body + where + absorbAbs :: Int -> Expression -> Doc + -- If there are multiple ID parameters to that function, treat them all at once + absorbAbs depth (Abstraction (IDParameter param0) colon0 body0) = + hardspace <> pretty param0 <> pretty colon0 <> absorbAbs (depth + 1) body0 + absorbAbs _ expr | isAbsorbableExpr expr = hardspace <> group' Priority (absorbExpr False expr) + -- Force the content onto a new line when it is not absorbable and there are more than two arguments + absorbAbs depth x = + (if depth <= 2 then line else hardline) <> pretty x + + -- Attrset parameter + pretty (Abstraction param colon (Term t)) + | isAbsorbable t = + pretty param <> pretty colon <> line <> group (prettyTermWide t) + pretty (Abstraction param colon body) = + pretty param <> pretty colon <> line <> pretty body + pretty (Application f a) = + prettyApp False mempty False f a + -- not chainable binary operators: <, >, <=, >=, ==, != + pretty (Operation a op@(Ann _ op' _) b) + | op' == TLess || op' == TGreater || op' == TLessEqual || op' == TGreaterEqual || op' == TEqual || op' == TUnequal = + pretty a <> softline <> pretty op <> hardspace <> pretty b + -- all other operators + pretty operation@(Operation _ op _) = + let -- Walk the operation tree and put a list of things on the same level. + -- We still need to keep the operators around because they might have comments attached to them. + -- An operator is put together with its succeeding expression. Only the first operand has none. + flatten :: Maybe Leaf -> Expression -> [(Maybe Leaf, Expression)] + flatten opL (Operation a opR b) | opR == op = flatten opL a ++ flatten (Just opR) b + flatten opL x = [(opL, x)] + + -- Called on every operand except the first one (a.k.a. RHS) + absorbOperation :: Expression -> Doc + absorbOperation (Term t) | isAbsorbable t = hardspace <> pretty t + -- Force nested operations to start on a new line + absorbOperation x@(Operation{}) = group' RegularG $ line <> pretty x + -- Force applications to start on a new line if more than the last argument is multiline + absorbOperation (Application f a) = group $ prettyApp False line False f a + absorbOperation x = hardspace <> pretty x + + prettyOperation :: (Maybe Leaf, Expression) -> Doc + -- First element + prettyOperation (Nothing, expr) = pretty expr + -- The others + prettyOperation (Just op', expr) = + line <> pretty (moveTrailingCommentUp op') <> nest (absorbOperation expr) + in group' RegularG $ + (concatMap prettyOperation . flatten Nothing) operation + pretty (MemberCheck expr qmark sel) = + pretty expr + <> softline + <> pretty qmark + <> hardspace + <> hcat sel + pretty (Negation minus expr) = + pretty minus <> pretty expr + pretty (Inversion bang expr) = + pretty bang <> pretty expr + +instance (Pretty a) => Pretty (Whole a) where + pretty (Whole x finalTrivia) = + group $ pretty x <> pretty finalTrivia instance Pretty Token where - pretty = text . tokenText - + pretty = text . tokenText isSimpleSelector :: Selector -> Bool isSimpleSelector (Selector _ (IDSelector _)) = True -isSimpleSelector _ = False +isSimpleSelector _ = False isSimple :: Expression -> Bool isSimple (Term (SimpleString (Ann [] _ Nothing))) = True isSimple (Term (IndentedString (Ann [] _ Nothing))) = True isSimple (Term (Path (Ann [] _ Nothing))) = True isSimple (Term (Token (Ann [] (Identifier _) Nothing))) = True -isSimple (Term (Selection t selectors def)) - = isSimple (Term t) && all isSimpleSelector selectors && isNothing def +isSimple (Term (Selection t selectors def)) = + isSimple (Term t) && all isSimpleSelector selectors && isNothing def isSimple (Term (Parenthesized (Ann [] _ Nothing) e (Ann [] _ Nothing))) = isSimple e -- Function applications of simple terms are simple up to two arguments isSimple (Application (Application (Application _ _) _) _) = False @@ -618,79 +697,78 @@ isSimple _ = False -- STRINGS instance Pretty StringPart where - pretty (TextPart t) = text t - - -- Absorb terms - -- This is exceedingly rare (why would one do this anyways?); one instance in the entire Nixpkgs - pretty (Interpolation (Whole (Term t) [])) - | isAbsorbable t - = group $ text "${" <> prettyTerm t <> text "}" - - -- For "simple" interpolations (see isSimple, but mostly just identifiers), - -- force onto one line, regardless of length - pretty (Interpolation (Whole expr [])) - | isSimple expr - = text "${" <> fromMaybe (pretty expr) (unexpandSpacing' Nothing (pretty expr)) <> text "}" - - -- For interpolations, we try to render the content, to see how long it will be. - -- If the interpolation is single-line and shorter than 30 characters, we force it - -- onto that line, even if this would make it go over the line limit. - pretty (Interpolation whole) = - group $ text "${" <> inner <> text "}" - where - whole' = pretty whole - inner = fromMaybe - -- default - (surroundWith line' $ nest whole') - -- force on one line if possible - (unexpandSpacing' (Just 30) whole') + pretty (TextPart t) = text t + -- Absorb terms + -- This is exceedingly rare (why would one do this anyways?); one instance in the entire Nixpkgs + pretty (Interpolation (Whole (Term t) [])) + | isAbsorbable t = + group $ text "${" <> prettyTerm t <> text "}" + -- For "simple" interpolations (see isSimple, but mostly just identifiers), + -- force onto one line, regardless of length + pretty (Interpolation (Whole expr [])) + | isSimple expr = + text "${" <> fromMaybe (pretty expr) (unexpandSpacing' Nothing (pretty expr)) <> text "}" + -- For interpolations, we try to render the content, to see how long it will be. + -- If the interpolation is single-line and shorter than 30 characters, we force it + -- onto that line, even if this would make it go over the line limit. + pretty (Interpolation whole) = + group $ text "${" <> inner <> text "}" + where + whole' = pretty whole + inner = + fromMaybe + -- default + (surroundWith line' $ nest whole') + -- force on one line if possible + (unexpandSpacing' (Just 30) whole') instance Pretty [StringPart] where - -- When the interpolation is the only thing on the string line, - -- then absorb the content (i.e. don't surround with line'). - -- Only do this when there are no comments - pretty [Interpolation (Whole expr [])] - = group $ text "${" <> nest inner <> text "}" - where - -- Code copied over from parentheses. Could be factored out into a common function one day - inner = case expr of - -- Start on the same line for these - _ | isAbsorbableExpr expr -> group $ absorbExpr False expr - -- Parenthesized application - (Application f a) -> prettyApp True mempty True f a - -- Same thing for selections - (Term (Selection t _ _)) | isAbsorbable t -> line' <> group expr <> line' - (Term (Selection {})) -> group expr <> line' - -- Start on a new line for the others - _ -> line' <> group expr <> line' - - -- Fallback case: there are some comments around it. Always surround with line' then - pretty [Interpolation expr] - = group $ text "${" <> surroundWith line' (nest expr) <> text "}" - - -- If we split a string line over multiple code lines due to large - -- interpolations, make sure to indent based on the indentation of the line - -- in the string. - pretty (TextPart t : parts) - = text t <> offset indentation (hcat parts) - where indentation = textWidth $ Text.takeWhile isSpace t - - pretty parts = hcat parts + -- When the interpolation is the only thing on the string line, + -- then absorb the content (i.e. don't surround with line'). + -- Only do this when there are no comments + pretty [Interpolation (Whole expr [])] = + group $ text "${" <> nest inner <> text "}" + where + -- Code copied over from parentheses. Could be factored out into a common function one day + inner = case expr of + -- Start on the same line for these + _ | isAbsorbableExpr expr -> group $ absorbExpr False expr + -- Parenthesized application + (Application f a) -> prettyApp True mempty True f a + -- Same thing for selections + (Term (Selection t _ _)) | isAbsorbable t -> line' <> group expr <> line' + (Term (Selection{})) -> group expr <> line' + -- Start on a new line for the others + _ -> line' <> group expr <> line' + + -- Fallback case: there are some comments around it. Always surround with line' then + pretty [Interpolation expr] = + group $ text "${" <> surroundWith line' (nest expr) <> text "}" + -- If we split a string line over multiple code lines due to large + -- interpolations, make sure to indent based on the indentation of the line + -- in the string. + pretty (TextPart t : parts) = + text t <> offset indentation (hcat parts) + where + indentation = textWidth $ Text.takeWhile isSpace t + pretty parts = hcat parts prettySimpleString :: [[StringPart]] -> Doc -prettySimpleString parts = group $ +prettySimpleString parts = + group $ text "\"" - -- Use literal \n here instead of `newline`, as the latter - -- would cause multiline-string-style indentation which we do not want - <> sepBy (text "\n") (map pretty parts) - <> text "\"" + -- Use literal \n here instead of `newline`, as the latter + -- would cause multiline-string-style indentation which we do not want + <> sepBy (text "\n") (map pretty parts) + <> text "\"" prettyIndentedString :: [[StringPart]] -> Doc -prettyIndentedString parts = group $ +prettyIndentedString parts = + group $ text "''" - -- Usually the `''` is followed by a potential line break. - -- However, for single-line strings it should be omitted, because often times a line break will - -- not reduce the indentation at all - <> (case parts of { _:_:_ -> line'; _ -> mempty }) - <> nest (sepBy newline $ map pretty parts) - <> text "''" + -- Usually the `''` is followed by a potential line break. + -- However, for single-line strings it should be omitted, because often times a line break will + -- not reduce the indentation at all + <> (case parts of _ : _ : _ -> line'; _ -> mempty) + <> nest (sepBy newline $ map pretty parts) + <> text "''" diff --git a/src/Nixfmt/Types.hs b/src/Nixfmt/Types.hs index 7307f58d..4afc6351 100644 --- a/src/Nixfmt/Types.hs +++ b/src/Nixfmt/Types.hs @@ -1,18 +1,21 @@ -{-# LANGUAGE DeriveFoldable, OverloadedStrings, RankNTypes, LambdaCase, FlexibleInstances #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} module Nixfmt.Types where -import Prelude hiding (String) - -import Data.Maybe (maybeToList) -import Data.List.NonEmpty as NonEmpty import Control.Monad.State (StateT) import Data.Bifunctor (first) import Data.Foldable (toList) import Data.Function (on) +import Data.List.NonEmpty as NonEmpty +import Data.Maybe (maybeToList) import Data.Text (Text, pack) import Data.Void (Void) import qualified Text.Megaparsec as MP (ParseErrorBundle, Parsec) +import Prelude hiding (String) -- | A @megaparsec@ @ParsecT@ specified for use with @nixfmt@. type Parser = StateT Trivia (MP.Parsec Void Text) @@ -21,22 +24,22 @@ type Parser = StateT Trivia (MP.Parsec Void Text) type ParseErrorBundle = MP.ParseErrorBundle Text Void data Trivium - = EmptyLine - -- Single line comments, either with # or /*. (We don't need to track which one it is, + = EmptyLine + | -- Single line comments, either with # or /*. (We don't need to track which one it is, -- as they will all be normalized to # comments. - | LineComment Text - -- Multi-line comments with /* or /**. Multiple # comments are treated as a list of `LineComment`. + LineComment Text + | -- Multi-line comments with /* or /**. Multiple # comments are treated as a list of `LineComment`. -- The bool indicates a doc comment (/**) - | BlockComment Bool [Text] - deriving (Eq, Show) + BlockComment Bool [Text] + deriving (Eq, Show) type Trivia = [Trivium] newtype TrailingComment = TrailingComment Text deriving (Eq, Show) data Ann a - = Ann Trivia a (Maybe TrailingComment) - deriving (Show) + = Ann Trivia a (Maybe TrailingComment) + deriving (Show) hasTrivia :: Ann a -> Bool hasTrivia (Ann [] _ Nothing) = False @@ -47,37 +50,37 @@ ann a = Ann [] a Nothing -- | Equality of annotated syntax is defined as equality of their corresponding -- semantics, thus ignoring the annotations. -instance Eq a => Eq (Ann a) where - Ann _ x _ == Ann _ y _ = x == y +instance (Eq a) => Eq (Ann a) where + Ann _ x _ == Ann _ y _ = x == y -- Trivia is ignored for Eq, so also don't show ---instance Show a => Show (Ann a) where +-- instance Show a => Show (Ann a) where -- show (Ann _ a _) = show a data Item a - -- | An item with a list of line comments that apply to it. There is no + = -- | An item with a list of line comments that apply to it. There is no -- empty line between the comments and the stuff it applies to. - = CommentedItem Trivia a - -- | A list of line comments not associated with any item. Followed by an + CommentedItem Trivia a + | -- | A list of line comments not associated with any item. Followed by an -- empty line unless they're the last comments in a set or list. - | DetachedComments Trivia - deriving (Foldable, Show) + DetachedComments Trivia + deriving (Foldable, Show) -newtype Items a = Items { unItems :: [Item a] } +newtype Items a = Items {unItems :: [Item a]} -instance Eq a => Eq (Items a) where - (==) = (==) `on` concatMap Data.Foldable.toList . unItems +instance (Eq a) => Eq (Items a) where + (==) = (==) `on` concatMap Data.Foldable.toList . unItems -instance Show a => Show (Items a) where - -- show = show . concatMap Data.Foldable.toList . unItems - show = show . unItems +instance (Show a) => Show (Items a) where + -- show = show . concatMap Data.Foldable.toList . unItems + show = show . unItems type Leaf = Ann Token data StringPart - = TextPart Text - | Interpolation (Whole Expression) - deriving (Eq, Show) + = TextPart Text + | Interpolation (Whole Expression) + deriving (Eq, Show) type Path = Ann [StringPart] @@ -87,438 +90,430 @@ type Path = Ann [StringPart] type String = Ann [[StringPart]] data SimpleSelector - = IDSelector Leaf - | InterpolSelector (Ann StringPart) - | StringSelector String - deriving (Eq, Show) + = IDSelector Leaf + | InterpolSelector (Ann StringPart) + | StringSelector String + deriving (Eq, Show) data Selector - -- `.selector` - = Selector (Maybe Leaf) SimpleSelector - deriving (Eq, Show) + = -- `.selector` + Selector (Maybe Leaf) SimpleSelector + deriving (Eq, Show) data Binder - = Inherit Leaf (Maybe Term) [SimpleSelector] Leaf - | Assignment [Selector] Leaf Expression Leaf - deriving (Eq, Show) + = Inherit Leaf (Maybe Term) [SimpleSelector] Leaf + | Assignment [Selector] Leaf Expression Leaf + deriving (Eq, Show) data Term - = Token Leaf - -- " String - | SimpleString String - -- '' String - | IndentedString String - | Path Path - | List Leaf (Items Term) Leaf - | Set (Maybe Leaf) Leaf (Items Binder) Leaf - | Selection Term [Selector] (Maybe (Leaf, Term)) - | Parenthesized Leaf Expression Leaf - deriving (Eq, Show) + = Token Leaf + | -- " String + SimpleString String + | -- '' String + IndentedString String + | Path Path + | List Leaf (Items Term) Leaf + | Set (Maybe Leaf) Leaf (Items Binder) Leaf + | Selection Term [Selector] (Maybe (Leaf, Term)) + | Parenthesized Leaf Expression Leaf + deriving (Eq, Show) data ParamAttr - -- name, Maybe question mark and default, maybe comma - = ParamAttr Leaf (Maybe (Leaf, Expression)) (Maybe Leaf) - | ParamEllipsis Leaf - deriving (Eq, Show) + = -- name, Maybe question mark and default, maybe comma + ParamAttr Leaf (Maybe (Leaf, Expression)) (Maybe Leaf) + | ParamEllipsis Leaf + deriving (Eq, Show) data Parameter - = IDParameter Leaf - | SetParameter Leaf [ParamAttr] Leaf - | ContextParameter Parameter Leaf Parameter - deriving (Show) + = IDParameter Leaf + | SetParameter Leaf [ParamAttr] Leaf + | ContextParameter Parameter Leaf Parameter + deriving (Show) instance Eq Parameter where - (IDParameter l) == (IDParameter r) = l == r - (SetParameter l1 l2 l3) == (SetParameter r1 r2 r3) = - l1 == r1 - && cmp l2 r2 - && l3 == r3 - where - -- Compare two lists of paramters, but for the last argument don't compare whether or not there is a trailing comma - cmp [] [] = True - cmp [ParamAttr x1 x2 _] [ParamAttr y1 y2 _] = x1 == y1 && x2 == y2 - cmp (x:xs) (y:ys) = x == y && cmp xs ys - cmp _ _ = False - (ContextParameter l1 l2 l3) == (ContextParameter r1 r2 r3) = l1 == r1 && l2 == r2 && l3 == r3 - _ == _ = False + (IDParameter l) == (IDParameter r) = l == r + (SetParameter l1 l2 l3) == (SetParameter r1 r2 r3) = + l1 == r1 + && cmp l2 r2 + && l3 == r3 + where + -- Compare two lists of paramters, but for the last argument don't compare whether or not there is a trailing comma + cmp [] [] = True + cmp [ParamAttr x1 x2 _] [ParamAttr y1 y2 _] = x1 == y1 && x2 == y2 + cmp (x : xs) (y : ys) = x == y && cmp xs ys + cmp _ _ = False + (ContextParameter l1 l2 l3) == (ContextParameter r1 r2 r3) = l1 == r1 && l2 == r2 && l3 == r3 + _ == _ = False data Expression - = Term Term - | With Leaf Expression Leaf Expression - | Let Leaf (Items Binder) Leaf Expression - | Assert Leaf Expression Leaf Expression - | If Leaf Expression Leaf Expression Leaf Expression - | Abstraction Parameter Leaf Expression - - | Application Expression Expression - | Operation Expression Leaf Expression - | MemberCheck Expression Leaf [Selector] - | Negation Leaf Expression - | Inversion Leaf Expression - deriving (Eq, Show) + = Term Term + | With Leaf Expression Leaf Expression + | Let Leaf (Items Binder) Leaf Expression + | Assert Leaf Expression Leaf Expression + | If Leaf Expression Leaf Expression Leaf Expression + | Abstraction Parameter Leaf Expression + | Application Expression Expression + | Operation Expression Leaf Expression + | MemberCheck Expression Leaf [Selector] + | Negation Leaf Expression + | Inversion Leaf Expression + deriving (Eq, Show) -- | A Whole a is an a including final trivia. It's assumed the a stores the -- initial trivia. data Whole a - = Whole a Trivia + = Whole a Trivia -- | Equality of annotated syntax is defined as equality of their corresponding -- semantics, thus ignoring the annotations. -instance Eq a => Eq (Whole a) where - Whole x _ == Whole y _ = x == y +instance (Eq a) => Eq (Whole a) where + Whole x _ == Whole y _ = x == y -- Trivia is ignored for Eq, so also don't show -instance Show a => Show (Whole a) where - show (Whole a _) = show a +instance (Show a) => Show (Whole a) where + show (Whole a _) = show a type File = Whole Expression -- Implemented by all AST-related types whose values are guaranteed to contain at least one (annotated) token class LanguageElement a where - -- Map the first token of that expression, no matter how deep it sits - -- in the AST. This is useful for modifying comments - mapFirstToken :: (forall b. Ann b -> Ann b) -> a -> a - mapFirstToken f a = fst (mapFirstToken' (\x -> (f x, ())) a) + -- Map the first token of that expression, no matter how deep it sits + -- in the AST. This is useful for modifying comments + mapFirstToken :: (forall b. Ann b -> Ann b) -> a -> a + mapFirstToken f a = fst (mapFirstToken' (\x -> (f x, ())) a) - -- Same as mapFirstToken, but the mapping function also yields a value that may be - -- returned. This is useful for getting/extracting values - mapFirstToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c) + -- Same as mapFirstToken, but the mapping function also yields a value that may be + -- returned. This is useful for getting/extracting values + mapFirstToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c) - -- Map the last token of that expression, no matter how deep it sits - -- in the AST. This is useful for modifying comments - mapLastToken :: (forall b. Ann b -> Ann b) -> a -> a - mapLastToken f a = fst (mapLastToken' (\x -> (f x, ())) a) + -- Map the last token of that expression, no matter how deep it sits + -- in the AST. This is useful for modifying comments + mapLastToken :: (forall b. Ann b -> Ann b) -> a -> a + mapLastToken f a = fst (mapLastToken' (\x -> (f x, ())) a) - -- Same as mapLastToken, but the mapping function also yields a value that may be - -- returned. This is useful for getting/extracting values - mapLastToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c) + -- Same as mapLastToken, but the mapping function also yields a value that may be + -- returned. This is useful for getting/extracting values + mapLastToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c) - -- Walk all syntactically valid sub-expressions in a breadth-first search way. This allows - -- minimizing failing test cases - walkSubprograms :: a -> [Expression] + -- Walk all syntactically valid sub-expressions in a breadth-first search way. This allows + -- minimizing failing test cases + walkSubprograms :: a -> [Expression] instance LanguageElement (Ann a) where - mapFirstToken' f = f - mapLastToken' f = f - walkSubprograms = error "unreachable" + mapFirstToken' f = f + mapLastToken' f = f + walkSubprograms = error "unreachable" instance LanguageElement SimpleSelector where - mapFirstToken' f = \case - (IDSelector name) -> first IDSelector $ f name - (InterpolSelector name) -> first InterpolSelector $ f name - (StringSelector name) -> first StringSelector $ f name + mapFirstToken' f = \case + (IDSelector name) -> first IDSelector $ f name + (InterpolSelector name) -> first InterpolSelector $ f name + (StringSelector name) -> first StringSelector $ f name - mapLastToken' = mapFirstToken' + mapLastToken' = mapFirstToken' - walkSubprograms = \case - (IDSelector name) -> [Term (Token name)] - (InterpolSelector (Ann _ str _)) -> pure $ Term $ SimpleString $ Ann [] [[str]] Nothing - (StringSelector str) -> [Term (SimpleString str)] + walkSubprograms = \case + (IDSelector name) -> [Term (Token name)] + (InterpolSelector (Ann _ str _)) -> pure $ Term $ SimpleString $ Ann [] [[str]] Nothing + (StringSelector str) -> [Term (SimpleString str)] instance LanguageElement Selector where - mapFirstToken' f (Selector Nothing ident) = first (Selector Nothing) $ mapFirstToken' f ident - mapFirstToken' f (Selector (Just dot) ident) = first (\dot' -> Selector (Just dot') ident) $ mapFirstToken' f dot + mapFirstToken' f (Selector Nothing ident) = first (Selector Nothing) $ mapFirstToken' f ident + mapFirstToken' f (Selector (Just dot) ident) = first (\dot' -> Selector (Just dot') ident) $ mapFirstToken' f dot - mapLastToken' f (Selector dot ident) = first (Selector dot) $ mapLastToken' f ident + mapLastToken' f (Selector dot ident) = first (Selector dot) $ mapLastToken' f ident - walkSubprograms (Selector _ ident) = walkSubprograms ident + walkSubprograms (Selector _ ident) = walkSubprograms ident instance LanguageElement ParamAttr where - mapFirstToken' _ _ = error "unreachable" - mapLastToken' _ _ = error "unreachable" + mapFirstToken' _ _ = error "unreachable" + mapLastToken' _ _ = error "unreachable" - walkSubprograms = \case - (ParamAttr name Nothing _) -> [Term (Token name)] - (ParamAttr name (Just (_, def)) _) -> [Term (Token name), def] - (ParamEllipsis _) -> [] + walkSubprograms = \case + (ParamAttr name Nothing _) -> [Term (Token name)] + (ParamAttr name (Just (_, def)) _) -> [Term (Token name), def] + (ParamEllipsis _) -> [] instance LanguageElement Parameter where - mapFirstToken' f = \case - (IDParameter name) -> first IDParameter (f name) - (SetParameter open items close) -> first (\open' -> SetParameter open' items close) (f open) - (ContextParameter first' at second) -> first (\first'' -> ContextParameter first'' at second) (mapFirstToken' f first') + mapFirstToken' f = \case + (IDParameter name) -> first IDParameter (f name) + (SetParameter open items close) -> first (\open' -> SetParameter open' items close) (f open) + (ContextParameter first' at second) -> first (\first'' -> ContextParameter first'' at second) (mapFirstToken' f first') - mapLastToken' f = \case - (IDParameter name) -> first IDParameter (f name) - (SetParameter open items close) -> first (SetParameter open items) (f close) - (ContextParameter first' at second) -> first (ContextParameter first' at) (mapLastToken' f second) + mapLastToken' f = \case + (IDParameter name) -> first IDParameter (f name) + (SetParameter open items close) -> first (SetParameter open items) (f close) + (ContextParameter first' at second) -> first (ContextParameter first' at) (mapLastToken' f second) - walkSubprograms = \case - (IDParameter ident) -> [Term $ Token ident] - (SetParameter _ bindings _) -> bindings >>= walkSubprograms - (ContextParameter left _ right) -> walkSubprograms left ++ walkSubprograms right + walkSubprograms = \case + (IDParameter ident) -> [Term $ Token ident] + (SetParameter _ bindings _) -> bindings >>= walkSubprograms + (ContextParameter left _ right) -> walkSubprograms left ++ walkSubprograms right instance LanguageElement Term where - mapFirstToken' f = \case - (Token leaf) -> first Token (f leaf) - (SimpleString string) -> first SimpleString (f string) - (IndentedString string) -> first IndentedString (f string) - (Path path) -> first Path (f path) - (List open items close) -> first (\open' -> List open' items close) (f open) - (Set (Just rec) open items close) -> first (\rec' -> Set (Just rec') open items close) (f rec) - (Set Nothing open items close) -> first (\open' -> Set Nothing open' items close) (f open) - (Selection term selector def) -> first (\term' -> Selection term' selector def) (mapFirstToken' f term) - (Parenthesized open expr close) -> first (\open' -> Parenthesized open' expr close) (f open) - - mapLastToken' f = \case - (Token leaf) -> first Token (f leaf) - (SimpleString string) -> first SimpleString (f string) - (IndentedString string) -> first IndentedString (f string) - (Path path) -> first Path (f path) - (List open items close) -> first (List open items) (f close) - (Set rec open items close) -> first (Set rec open items) (f close) - (Selection term sels (Just (orToken, def))) -> first (\def' -> Selection term sels (Just (orToken, def'))) (mapLastToken' f def) - (Selection term sels Nothing) -> - case NonEmpty.nonEmpty sels of - Just nonEmptySels -> first (\sels' -> Selection term (NonEmpty.toList sels') Nothing) (mapLastToken' f nonEmptySels) - Nothing -> first (\term' -> Selection term' [] Nothing) (mapLastToken' f term) - (Parenthesized open expr close) -> first (Parenthesized open expr) (f close) - - walkSubprograms = \case - -- Map each item to a singleton list, then handle that - (List _ items _) | Prelude.length (unItems items) == 1 -> case Prelude.head (unItems items) of - (CommentedItem c item) -> [emptySet c, Term item] - (DetachedComments _) -> [] - (List _ items _) -> unItems items >>= \case - CommentedItem comment item -> - [ Term (List (ann TBrackOpen) (Items [CommentedItem comment item]) (ann TBrackClose)) ] - DetachedComments c -> - [ Term (List (ann TBrackOpen) (Items [DetachedComments c]) (ann TBrackClose)) ] - - (Set _ _ items _) | Prelude.length (unItems items) == 1 -> case Prelude.head (unItems items) of - (CommentedItem c (Inherit _ from sels _)) -> - (Term <$> maybeToList from) ++ concatMap walkSubprograms sels ++ [emptySet c] - (CommentedItem c (Assignment sels _ expr _)) -> - expr : concatMap walkSubprograms sels ++ [emptySet c] - (DetachedComments _) -> [] - (Set _ _ items _) -> unItems items >>= \case - -- Map each binding to a singleton set - (CommentedItem comment item) -> - [ Term (Set Nothing (ann TBraceOpen) (Items [CommentedItem comment item]) (ann TBraceClose)) ] - (DetachedComments c) -> [ emptySet c ] - (Selection term sels Nothing) -> Term term : (sels >>= walkSubprograms) - (Selection term sels (Just (_, def))) -> Term term : (sels >>= walkSubprograms) ++ [ Term def ] - (Parenthesized _ expr _) -> [expr] - -- The others are already minimal - _ -> [] - where - emptySet c = Term (Set Nothing (ann TBraceOpen) (Items [DetachedComments c]) (ann TBraceClose)) + mapFirstToken' f = \case + (Token leaf) -> first Token (f leaf) + (SimpleString string) -> first SimpleString (f string) + (IndentedString string) -> first IndentedString (f string) + (Path path) -> first Path (f path) + (List open items close) -> first (\open' -> List open' items close) (f open) + (Set (Just rec) open items close) -> first (\rec' -> Set (Just rec') open items close) (f rec) + (Set Nothing open items close) -> first (\open' -> Set Nothing open' items close) (f open) + (Selection term selector def) -> first (\term' -> Selection term' selector def) (mapFirstToken' f term) + (Parenthesized open expr close) -> first (\open' -> Parenthesized open' expr close) (f open) + + mapLastToken' f = \case + (Token leaf) -> first Token (f leaf) + (SimpleString string) -> first SimpleString (f string) + (IndentedString string) -> first IndentedString (f string) + (Path path) -> first Path (f path) + (List open items close) -> first (List open items) (f close) + (Set rec open items close) -> first (Set rec open items) (f close) + (Selection term sels (Just (orToken, def))) -> first (\def' -> Selection term sels (Just (orToken, def'))) (mapLastToken' f def) + (Selection term sels Nothing) -> + case NonEmpty.nonEmpty sels of + Just nonEmptySels -> first (\sels' -> Selection term (NonEmpty.toList sels') Nothing) (mapLastToken' f nonEmptySels) + Nothing -> first (\term' -> Selection term' [] Nothing) (mapLastToken' f term) + (Parenthesized open expr close) -> first (Parenthesized open expr) (f close) + + walkSubprograms = \case + -- Map each item to a singleton list, then handle that + (List _ items _) | Prelude.length (unItems items) == 1 -> case Prelude.head (unItems items) of + (CommentedItem c item) -> [emptySet c, Term item] + (DetachedComments _) -> [] + (List _ items _) -> + unItems items >>= \case + CommentedItem comment item -> + [Term (List (ann TBrackOpen) (Items [CommentedItem comment item]) (ann TBrackClose))] + DetachedComments c -> + [Term (List (ann TBrackOpen) (Items [DetachedComments c]) (ann TBrackClose))] + (Set _ _ items _) | Prelude.length (unItems items) == 1 -> case Prelude.head (unItems items) of + (CommentedItem c (Inherit _ from sels _)) -> + (Term <$> maybeToList from) ++ concatMap walkSubprograms sels ++ [emptySet c] + (CommentedItem c (Assignment sels _ expr _)) -> + expr : concatMap walkSubprograms sels ++ [emptySet c] + (DetachedComments _) -> [] + (Set _ _ items _) -> + unItems items >>= \case + -- Map each binding to a singleton set + (CommentedItem comment item) -> + [Term (Set Nothing (ann TBraceOpen) (Items [CommentedItem comment item]) (ann TBraceClose))] + (DetachedComments c) -> [emptySet c] + (Selection term sels Nothing) -> Term term : (sels >>= walkSubprograms) + (Selection term sels (Just (_, def))) -> Term term : (sels >>= walkSubprograms) ++ [Term def] + (Parenthesized _ expr _) -> [expr] + -- The others are already minimal + _ -> [] + where + emptySet c = Term (Set Nothing (ann TBraceOpen) (Items [DetachedComments c]) (ann TBraceClose)) instance LanguageElement Expression where - mapFirstToken' f = \case - (Term term) -> first Term (mapFirstToken' f term) - (With with expr0 semicolon expr1) -> first (\with' -> With with' expr0 semicolon expr1) (f with) - (Let let_ items in_ body) -> first (\let_' -> Let let_' items in_ body) (f let_) - (Assert assert cond semicolon body) -> first (\assert' -> Assert assert' cond semicolon body) (f assert) - (If if_ expr0 then_ expr1 else_ expr2) -> first (\if_' -> If if_' expr0 then_ expr1 else_ expr2) (f if_) - (Abstraction param colon body) -> first (\param' -> Abstraction param' colon body) (mapFirstToken' f param) - (Application g a) -> first (`Application` a) (mapFirstToken' f g) - (Operation left op right) -> first (\left' -> Operation left' op right) (mapFirstToken' f left) - (MemberCheck name dot selectors) -> first (\name' -> MemberCheck name' dot selectors) (mapFirstToken' f name) - (Negation not_ expr) -> first (`Negation` expr) (f not_) - (Inversion tilde expr) -> first (`Inversion` expr) (f tilde) - - mapLastToken' f = \case - (Term term) -> first Term (mapLastToken' f term) - (With with expr0 semicolon expr1) -> first (With with expr0 semicolon) (mapLastToken' f expr1) - (Let let_ items in_ body) -> first (Let let_ items in_) (mapLastToken' f body) - (Assert assert cond semicolon body) -> first (Assert assert cond semicolon) (mapLastToken' f body) - (If if_ expr0 then_ expr1 else_ expr2) -> first (If if_ expr0 then_ expr1 else_) (mapLastToken' f expr2) - (Abstraction param colon body) -> first (Abstraction param colon) (mapLastToken' f body) - (Application g a) -> first (Application g) (mapLastToken' f a) - (Operation left op right) -> first (Operation left op) (mapLastToken' f right) - (MemberCheck name dot []) -> first (\dot' -> MemberCheck name dot' []) (mapLastToken' f dot) - (MemberCheck name dot sels) -> first (MemberCheck name dot . NonEmpty.toList) (mapLastToken' f $ NonEmpty.fromList sels) - (Negation not_ expr) -> first (Negation not_) (mapLastToken' f expr) - (Inversion tilde expr) -> first (Inversion tilde) (mapLastToken' f expr) - - walkSubprograms = \case - (Term term) -> walkSubprograms term - (With _ expr0 _ expr1) -> [expr0, expr1] - (Let _ items _ body) -> body : (unItems items >>= \case - -- Map each binding to a singleton set - (CommentedItem _ item) -> [ Term (Set Nothing (ann TBraceOpen) (Items [CommentedItem [] item]) (ann TBraceClose)) ] - (DetachedComments _) -> [] - ) - (Assert _ cond _ body) -> [cond, body] - (If _ expr0 _ expr1 _ expr2) -> [expr0, expr1, expr2] - (Abstraction param _ body) -> [Abstraction param (ann TColon) (Term (Token (ann (Identifier "_")))), body] - (Application g a) -> [g, a] - (Operation left _ right) -> [left, right] - (MemberCheck name _ sels) -> name : (sels >>= walkSubprograms) - (Negation _ expr) -> [expr] - (Inversion _ expr) -> [expr] + mapFirstToken' f = \case + (Term term) -> first Term (mapFirstToken' f term) + (With with expr0 semicolon expr1) -> first (\with' -> With with' expr0 semicolon expr1) (f with) + (Let let_ items in_ body) -> first (\let_' -> Let let_' items in_ body) (f let_) + (Assert assert cond semicolon body) -> first (\assert' -> Assert assert' cond semicolon body) (f assert) + (If if_ expr0 then_ expr1 else_ expr2) -> first (\if_' -> If if_' expr0 then_ expr1 else_ expr2) (f if_) + (Abstraction param colon body) -> first (\param' -> Abstraction param' colon body) (mapFirstToken' f param) + (Application g a) -> first (`Application` a) (mapFirstToken' f g) + (Operation left op right) -> first (\left' -> Operation left' op right) (mapFirstToken' f left) + (MemberCheck name dot selectors) -> first (\name' -> MemberCheck name' dot selectors) (mapFirstToken' f name) + (Negation not_ expr) -> first (`Negation` expr) (f not_) + (Inversion tilde expr) -> first (`Inversion` expr) (f tilde) + + mapLastToken' f = \case + (Term term) -> first Term (mapLastToken' f term) + (With with expr0 semicolon expr1) -> first (With with expr0 semicolon) (mapLastToken' f expr1) + (Let let_ items in_ body) -> first (Let let_ items in_) (mapLastToken' f body) + (Assert assert cond semicolon body) -> first (Assert assert cond semicolon) (mapLastToken' f body) + (If if_ expr0 then_ expr1 else_ expr2) -> first (If if_ expr0 then_ expr1 else_) (mapLastToken' f expr2) + (Abstraction param colon body) -> first (Abstraction param colon) (mapLastToken' f body) + (Application g a) -> first (Application g) (mapLastToken' f a) + (Operation left op right) -> first (Operation left op) (mapLastToken' f right) + (MemberCheck name dot []) -> first (\dot' -> MemberCheck name dot' []) (mapLastToken' f dot) + (MemberCheck name dot sels) -> first (MemberCheck name dot . NonEmpty.toList) (mapLastToken' f $ NonEmpty.fromList sels) + (Negation not_ expr) -> first (Negation not_) (mapLastToken' f expr) + (Inversion tilde expr) -> first (Inversion tilde) (mapLastToken' f expr) + + walkSubprograms = \case + (Term term) -> walkSubprograms term + (With _ expr0 _ expr1) -> [expr0, expr1] + (Let _ items _ body) -> + body + : ( unItems items >>= \case + -- Map each binding to a singleton set + (CommentedItem _ item) -> [Term (Set Nothing (ann TBraceOpen) (Items [CommentedItem [] item]) (ann TBraceClose))] + (DetachedComments _) -> [] + ) + (Assert _ cond _ body) -> [cond, body] + (If _ expr0 _ expr1 _ expr2) -> [expr0, expr1, expr2] + (Abstraction param _ body) -> [Abstraction param (ann TColon) (Term (Token (ann (Identifier "_")))), body] + (Application g a) -> [g, a] + (Operation left _ right) -> [left, right] + (MemberCheck name _ sels) -> name : (sels >>= walkSubprograms) + (Negation _ expr) -> [expr] + (Inversion _ expr) -> [expr] instance LanguageElement (Whole Expression) where - mapFirstToken' f (Whole a trivia) - = first (`Whole` trivia) (mapFirstToken' f a) + mapFirstToken' f (Whole a trivia) = + first (`Whole` trivia) (mapFirstToken' f a) - mapLastToken' f (Whole a trivia) - = first (`Whole` trivia) (mapLastToken' f a) + mapLastToken' f (Whole a trivia) = + first (`Whole` trivia) (mapLastToken' f a) - walkSubprograms (Whole a _) = [a] + walkSubprograms (Whole a _) = [a] -instance LanguageElement a => LanguageElement (NonEmpty a) where - mapFirstToken' f (x :| _) = first pure $ mapFirstToken' f x +instance (LanguageElement a) => LanguageElement (NonEmpty a) where + mapFirstToken' f (x :| _) = first pure $ mapFirstToken' f x - mapLastToken' f (x :| []) = first pure $ mapLastToken' f x - mapLastToken' f (x :| xs) = first ((x :|) . NonEmpty.toList) $ mapLastToken' f (NonEmpty.fromList xs) + mapLastToken' f (x :| []) = first pure $ mapLastToken' f x + mapLastToken' f (x :| xs) = first ((x :|) . NonEmpty.toList) $ mapLastToken' f (NonEmpty.fromList xs) - walkSubprograms = error "unreachable" + walkSubprograms = error "unreachable" data Token - = Integer Int - | Float Double - | Identifier Text - | EnvPath Text - - | KAssert - | KElse - | KIf - | KIn - | KInherit - | KLet - | KOr - | KRec - | KThen - | KWith - - | TBraceOpen - | TBraceClose - | TBrackOpen - | TBrackClose - | TInterOpen - | TInterClose - | TParenOpen - | TParenClose - - | TAssign - | TAt - | TColon - | TComma - | TDot - | TDoubleQuote - | TDoubleSingleQuote - | TEllipsis - | TQuestion - | TSemicolon - - | TConcat - | TNegate - | TUpdate - - | TPlus - | TMinus - | TMul - | TDiv - - | TAnd - | TOr - | TEqual - | TGreater - | TGreaterEqual - | TImplies - | TLess - | TLessEqual - | TNot - | TUnequal - - | SOF - deriving (Eq, Show) - + = Integer Int + | Float Double + | Identifier Text + | EnvPath Text + | KAssert + | KElse + | KIf + | KIn + | KInherit + | KLet + | KOr + | KRec + | KThen + | KWith + | TBraceOpen + | TBraceClose + | TBrackOpen + | TBrackClose + | TInterOpen + | TInterClose + | TParenOpen + | TParenClose + | TAssign + | TAt + | TColon + | TComma + | TDot + | TDoubleQuote + | TDoubleSingleQuote + | TEllipsis + | TQuestion + | TSemicolon + | TConcat + | TNegate + | TUpdate + | TPlus + | TMinus + | TMul + | TDiv + | TAnd + | TOr + | TEqual + | TGreater + | TGreaterEqual + | TImplies + | TLess + | TLessEqual + | TNot + | TUnequal + | SOF + deriving (Eq, Show) data Fixity - = Prefix - | InfixL - | InfixN - | InfixR - | Postfix - deriving (Eq, Show) + = Prefix + | InfixL + | InfixN + | InfixR + | Postfix + deriving (Eq, Show) data Operator - = Op Fixity Token - | Apply - deriving (Eq, Show) + = Op Fixity Token + | Apply + deriving (Eq, Show) -- | A list of lists of operators where lists that come first contain operators -- that bind more strongly. operators :: [[Operator]] operators = - [ [ Apply ] - , [ Op Prefix TMinus ] - , [ Op Postfix TQuestion ] - , [ Op InfixR TConcat ] - , [ Op InfixL TMul - , Op InfixL TDiv ] - , [ Op InfixL TPlus - , Op InfixL TMinus ] - , [ Op Prefix TNot ] - , [ Op InfixR TUpdate ] - , [ Op InfixN TLess - , Op InfixN TGreater - , Op InfixN TLessEqual - , Op InfixN TGreaterEqual ] - , [ Op InfixN TEqual - , Op InfixN TUnequal ] - , [ Op InfixL TAnd ] - , [ Op InfixL TOr ] - , [ Op InfixL TImplies ] - ] + [ [Apply], + [Op Prefix TMinus], + [Op Postfix TQuestion], + [Op InfixR TConcat], + [ Op InfixL TMul, + Op InfixL TDiv + ], + [ Op InfixL TPlus, + Op InfixL TMinus + ], + [Op Prefix TNot], + [Op InfixR TUpdate], + [ Op InfixN TLess, + Op InfixN TGreater, + Op InfixN TLessEqual, + Op InfixN TGreaterEqual + ], + [ Op InfixN TEqual, + Op InfixN TUnequal + ], + [Op InfixL TAnd], + [Op InfixL TOr], + [Op InfixL TImplies] + ] tokenText :: Token -> Text -tokenText (Identifier i) = i -tokenText (Integer i) = pack (show i) -tokenText (Float f) = pack (show f) -tokenText (EnvPath p) = "<" <> p <> ">" - -tokenText KAssert = "assert" -tokenText KElse = "else" -tokenText KIf = "if" -tokenText KIn = "in" -tokenText KInherit = "inherit" -tokenText KLet = "let" -tokenText KOr = "or" -tokenText KRec = "rec" -tokenText KThen = "then" -tokenText KWith = "with" - -tokenText TBraceOpen = "{" -tokenText TBraceClose = "}" -tokenText TBrackOpen = "[" -tokenText TBrackClose = "]" -tokenText TInterOpen = "${" -tokenText TInterClose = "}" -tokenText TParenOpen = "(" -tokenText TParenClose = ")" - -tokenText TAssign = "=" -tokenText TAt = "@" -tokenText TColon = ":" -tokenText TComma = "," -tokenText TDot = "." -tokenText TDoubleQuote = "\"" +tokenText (Identifier i) = i +tokenText (Integer i) = pack (show i) +tokenText (Float f) = pack (show f) +tokenText (EnvPath p) = "<" <> p <> ">" +tokenText KAssert = "assert" +tokenText KElse = "else" +tokenText KIf = "if" +tokenText KIn = "in" +tokenText KInherit = "inherit" +tokenText KLet = "let" +tokenText KOr = "or" +tokenText KRec = "rec" +tokenText KThen = "then" +tokenText KWith = "with" +tokenText TBraceOpen = "{" +tokenText TBraceClose = "}" +tokenText TBrackOpen = "[" +tokenText TBrackClose = "]" +tokenText TInterOpen = "${" +tokenText TInterClose = "}" +tokenText TParenOpen = "(" +tokenText TParenClose = ")" +tokenText TAssign = "=" +tokenText TAt = "@" +tokenText TColon = ":" +tokenText TComma = "," +tokenText TDot = "." +tokenText TDoubleQuote = "\"" tokenText TDoubleSingleQuote = "''" -tokenText TEllipsis = "..." -tokenText TQuestion = "?" -tokenText TSemicolon = ";" - -tokenText TPlus = "+" -tokenText TMinus = "-" -tokenText TMul = "*" -tokenText TDiv = "/" -tokenText TConcat = "++" -tokenText TNegate = "-" -tokenText TUpdate = "//" - -tokenText TAnd = "&&" -tokenText TOr = "||" -tokenText TEqual = "==" -tokenText TGreater = ">" -tokenText TGreaterEqual = ">=" -tokenText TImplies = "->" -tokenText TLess = "<" -tokenText TLessEqual = "<=" -tokenText TNot = "!" -tokenText TUnequal = "!=" - -tokenText SOF = "" +tokenText TEllipsis = "..." +tokenText TQuestion = "?" +tokenText TSemicolon = ";" +tokenText TPlus = "+" +tokenText TMinus = "-" +tokenText TMul = "*" +tokenText TDiv = "/" +tokenText TConcat = "++" +tokenText TNegate = "-" +tokenText TUpdate = "//" +tokenText TAnd = "&&" +tokenText TOr = "||" +tokenText TEqual = "==" +tokenText TGreater = ">" +tokenText TGreaterEqual = ">=" +tokenText TImplies = "->" +tokenText TLess = "<" +tokenText TLessEqual = "<=" +tokenText TNot = "!" +tokenText TUnequal = "!=" +tokenText SOF = "" diff --git a/src/Nixfmt/Util.hs b/src/Nixfmt/Util.hs index 31b55df6..8bc59866 100644 --- a/src/Nixfmt/Util.hs +++ b/src/Nixfmt/Util.hs @@ -1,23 +1,35 @@ - - -module Nixfmt.Util - ( manyP - , someP - , manyText - , someText - , commonIndentation - , identChar - , isSpaces - , pathChar - , schemeChar - , uriChar - ) where +module Nixfmt.Util ( + manyP, + someP, + manyText, + someText, + commonIndentation, + identChar, + isSpaces, + pathChar, + schemeChar, + uriChar, +) +where import Data.Char (isAlpha, isDigit, isSpace) -import Data.Text as Text - (Text, all, commonPrefixes, concat, empty, takeWhile) -import Text.Megaparsec - (MonadParsec, Token, Tokens, many, some, takeWhile1P, takeWhileP) +import Data.Text as Text ( + Text, + all, + commonPrefixes, + concat, + empty, + takeWhile, + ) +import Text.Megaparsec ( + MonadParsec, + Token, + Tokens, + many, + some, + takeWhile1P, + takeWhileP, + ) charClass :: [Char] -> Char -> Bool charClass s c = isAlpha c || isDigit c || elem c s @@ -35,34 +47,34 @@ uriChar :: Char -> Bool uriChar = charClass "~!@$%&*-=_+:',./?" -- | Match one or more characters that match a predicate. -someP :: MonadParsec e s m => (Token s -> Bool) -> m (Tokens s) +someP :: (MonadParsec e s m) => (Token s -> Bool) -> m (Tokens s) someP = takeWhile1P Nothing -- | Match zero or more characters that match a predicate. -manyP :: MonadParsec e s m => (Token s -> Bool) -> m (Tokens s) +manyP :: (MonadParsec e s m) => (Token s -> Bool) -> m (Tokens s) manyP = takeWhileP Nothing -- | Match one or more texts and return the concatenation. -someText :: MonadParsec e s m => m Text -> m Text +someText :: (MonadParsec e s m) => m Text -> m Text someText p = Text.concat <$> some p -- | Match zero or more texts and return the concatenation. -manyText :: MonadParsec e s m => m Text -> m Text +manyText :: (MonadParsec e s m) => m Text -> m Text manyText p = Text.concat <$> many p -- | The longest common prefix of the arguments. commonPrefix :: Text -> Text -> Text commonPrefix a b = - case commonPrefixes a b of - Nothing -> empty - Just (prefix, _, _) -> prefix + case commonPrefixes a b of + Nothing -> empty + Just (prefix, _, _) -> prefix -- | The longest common prefix consisting of only whitespace. The longest common -- prefix of zero texts is infinite, represented as Nothing. commonIndentation :: [Text] -> Maybe Text -commonIndentation [] = Nothing -commonIndentation [x] = Just $ Text.takeWhile isSpace x -commonIndentation (x:y:xs) = commonIndentation (commonPrefix x y : xs) +commonIndentation [] = Nothing +commonIndentation [x] = Just $ Text.takeWhile isSpace x +commonIndentation (x : y : xs) = commonIndentation (commonPrefix x y : xs) isSpaces :: Text -> Bool -isSpaces = Text.all (==' ') +isSpaces = Text.all (== ' ')