Skip to content

Automatic line breaking at specific limit#102

Closed
gustavoavena wants to merge 8 commits intofourmolu:mainfrom
gustavoavena:auto_line_breaking
Closed

Automatic line breaking at specific limit#102
gustavoavena wants to merge 8 commits intofourmolu:mainfrom
gustavoavena:auto_line_breaking

Conversation

@gustavoavena
Copy link
Contributor

@gustavoavena gustavoavena commented Jul 5, 2021

Discussed in #71. @georgefst this is the "hack" you suggested, so I would love to get your feedback if you have some time!

What

Provide an option to break lines automatically when they're over N characters long, where N is an optional integer provided in the config.

How

Fourmolu already has logic for how to break lines, since it does so when the user breaks some lines themselves. This PR adds a small hack in src/Ormolu/Printer/Combinators.hs to use the MultiLine Layout for a span if it's over N characters long.

Testing

Will add a file with some test cases in the next commit, but before putting up this PR I ran cabal test and ./format.sh, as instructed in the Contributing section. The ./format.sh script made some changes to the fourmolu.cabal file.

poNewlinesBetweenDecls :: f Int
poNewlinesBetweenDecls :: f Int,
-- | Max line length for automatic line breaking
poMaxLineLength :: f (Maybe Int)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create a new datatype for clarity:

data ColumnLimit = ColumnLimit Natural | NoLimit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Just did that and pushed the changes. Yaml config file is still not working though, so I'll look into that.

@gustavoavena
Copy link
Contributor Author

Friendly ping for @georgefst. Would you mind doing an initial review when you have some time? 🙂

@gustavoavena
Copy link
Contributor Author

Fixed the YAML config file parsing for my new option. AFAIK, the only remaining work (besides comments from review) is to add another test case in a 3rd commit.

@gustavoavena
Copy link
Contributor Author

When writing some test cases I ended up finding a bug in the implementation. Apparently, it can't properly break function arguments when they're over 80 characters long.

Here are some examples of what works and what doesn't work (i.e. I get an error because the AST changed after formatting).

-- ------------------------ These work ------------------------
-- ---------------------------------------------------------------80 characters|

longFunction0 :: String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String -> [String]
longFunction0 veryLongArg1 a b c d e f = ["a list", "of strings", "that will break the", "column limit"]

longFunction1 :: String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String
longFunction1 
    veryLongArg1 veryLongArg2 veryLongArg3 veryLongArg4 veryLongArgument5 veryLongArg6 = undefined

longFunction12 veryLongArg1 veryLongArg2 veryLongArg3 veryLongArg4 veryLongArgument5 
longFunction12 :: String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String -> String
    veryLongArg6 veryLongArg6 = undefined


-- If the arguments stop right at 80 characters, it works.
longFunction2 :: String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String
longFunction2 veryLongArg1 veryLongArg2 veryLongArg3 veryLongArg4 a veryLongArg6 =
    let aVeryLongLine = ["list one", "list one", "list one", "list one", "list one", "list one"]
    in undefined

-- ------------------------ Below here doesn't work ------------------------

longFunction3 :: String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String
longFunction3 veryLongArg1 veryLongArg2 veryLongArg3 veryLongArg4 veryLongArgument5 veryLongArg6 = undefined

I formatted this using the -u flag to see the output and the last example (the one that breaks) becomes this:

longFunction3 ::
    String -> String -> String -> Maybe Int -> Maybe Int -> Maybe Int -> String
longFunction3
veryLongArg1
veryLongArg2
veryLongArg3
veryLongArg4
veryLongArgument5
veryLongArg6 = undefined

I think that if the user doesn't break the arguments themselves, fourmolu can't distinguish the function name from the arguments... My only idea to make this work would be to disable this check for this specific case (function arguments). That would probably involve adding an argument to switchLayout, which calls spansLayout, to not use the column limit. The problem is that I don't have enough context on the code to find out where these functions will be called when parsing function definitions...

@expipiplus @parsonsmatt Could you maybe help me with that? Any code pointers that I could go over to disable the column limit in the switchLayout calls for function arguments?

@gustavoavena
Copy link
Contributor Author

I was actually able to find the place that I think switchLayout is used for the arguments list in the function definition. I just put up a 3rd commit with a fix that solved the issue.

@gustavoavena gustavoavena force-pushed the auto_line_breaking branch 2 times, most recently from ebab3cb to 66414db Compare July 7, 2021 16:43
@gustavoavena gustavoavena changed the title [RFC] Automatic line breaking at specific limit Automatic line breaking at specific limit Jul 7, 2021
@gustavoavena
Copy link
Contributor Author

I actually found a bug in this implementation that breaks idempotence in some cases... Since idempotence is an important property for a formatter, I don't want to ship this without a fix...
It seems to be related to the indentation of blocks after do statements in long lines (see examples at the bottom).

@georgefst, in one of your comments in #6 you said that "idempotence is a rather fragile thing". Were there any other features/improvements in the past that broke idempotence? If so, how were they fixed? I would appreciate anything that could help me debug and fix this issue.

Thanks!

Original code:

testFund :: Maybe Int
testFund = 
  firstTest oneFunctionArgument abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde $ do
    result <- aRandomResult

After formatting with column limit set to 80:

testFund :: Maybe Int
testFund =
    firstTest
        oneFunctionArgument
        abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde $ do
        result <- aRandomResult

After formatting the output again:

testFund :: Maybe Int
testFund =
    firstTest
        oneFunctionArgument
        abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
        $ do
            result <- aRandomResult

Of course, formatting this snippet with the -c flag to check idempotence fails.

@gustavoavena
Copy link
Contributor Author

I tested fourmolu with this option in thousands of files and found out that this isn't a big idempotence issue because it has a stopping point. i.e. the formatter won't undo something it did and get into a vicious cycle of doing/undoing (e.g. adding/removing a line break).

Also, this feature doesn't introduce any major issue that breaks the files (e.g. by changing their AST).

Considering what I've listed above and the fact that idempotence seems to be a very fragile property (IIRC, initial versions of Ormolu were not idempotent and it was still released and used with this known limitation), I was wondering if we could consider releasing this feature as long as it's clear that it has the limitation of affecting idempotence.

@kukimik, @brandonchinn178, I've seen you engage in other issues/PRs so I'm wondering if you have any thoughts about this? Any feedback would be very welcome! 🙂

@georgefst
Copy link
Collaborator

georgefst commented Aug 9, 2021

I'm alright with the idempotence issue seeing as:

  • we're fairly sure it doesn't get in to a loop
  • the documentation of --column-limit makes the issue clear (maybe it'd be nice to categorise the kind of inputs that cause issues, or give examples?)
  • the option is off by default

@brandonchinn178
Copy link
Collaborator

Are you saying its not idempotent? As in, running fourmolu multiple times could iteratively format line breaks? Can you give an example?

In general, though, I'm generally in favor of this. Specifically with regards to breaking import statements.

@gustavoavena
Copy link
Contributor Author

Thank you @georgefst @brandonchinn178 for your responses and feedback!

@georgefst, I'm happy to hear that! I just pushed one more commit where I add an example in the test files, update the test runner and add a line to the README making the column-limit limitation clearer. WDYT?

@brandonchinn178 yes, in some cases the formatter will not be idempotent. I just added an example here. In practice though, it doesn't look like the file will change when formatted for a 3rd time and the changes between the first and second formatting are very minor (e.g. indentation of statements nested inside a do or a $).

@gustavoavena
Copy link
Contributor Author

@georgefst LMK if there's anything else I need to add/change to merge this!
I'm very excited to ship this, so people that hate long lines (like me) don't have to worry about breaking them manually anymore 😬

@pepeiborra
Copy link

Is this ready to merge @brandonchinn178 @georgefst ?

@pepeiborra pepeiborra mentioned this pull request Sep 22, 2021
@gustavoavena gustavoavena force-pushed the auto_line_breaking branch 3 times, most recently from 5f6ab52 to 999b658 Compare October 5, 2021 14:30
@gustavoavena
Copy link
Contributor Author

Hey @georgefst! I noticed that there has been some significant changes committed in the past week, which is great!

I already rebased this PR and tested everything, so I'm wondering if we could get this merged?

@georgefst georgefst mentioned this pull request Oct 30, 2021
@ProofOfKeags
Copy link

Hi. What is needed for getting this merged? I want it :)

@brandonchinn178 brandonchinn178 added the new config option Adds or would add new configuration option label Jan 24, 2022
@brandonchinn178 brandonchinn178 force-pushed the master branch 12 times, most recently from c7364ed to 40bcc74 Compare May 18, 2022 21:03
@brandonchinn178
Copy link
Collaborator

brandonchinn178 commented Jun 1, 2022

👋 @gustavoavena
Thank you for opening this PR and being patient while we sorted out how to handle all the new contributions for new configuration options!

If you haven't noticed, we finally merged in a new testing framework for configuration options (#169)! This means we can finally address the backlog of open PRs.

Please do the following steps

  • Rebase against main

    git remote add upstream git@github.com:fourmolu/fourmolu
    git fetch upstream
    git rebase -i upstream/main
  • Resolve merge conflicts

  • Ensure a file has been added to changelog.d/ (see changelog.d/README.md)

  • Ensure configuration docs in README.md have been updated

  • Ensure fourmolu.yaml updated to stay in sync with config in README.md

  • Ensure tests have been added

You can use #135 as a reference for this checklist.

@gustavoavena
Copy link
Contributor Author

@brandonchinn178 Thank you for the update and for all the you've been putting in recently. Unfortunately I haven't had the bandwidth to get back to this, but once I have some more time, I'll work on rebasing and writing test to ship this :)

@arybczak
Copy link

Gentle ping - will work on getting this PR merged resume?

@brandonchinn178
Copy link
Collaborator

@arybczak no ones currently working on this PR. feel free to branch off and start a new PR

@CrystalSplitter
Copy link
Contributor

I took up on the offer and branched off on this work in the attempt to revive the feature in #305. Thank you for your work, @gustavoavena!

@georgefst
Copy link
Collaborator

This work has been merged via #305!

@georgefst georgefst closed this Apr 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new config option Adds or would add new configuration option

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants