lspitzner / brittany

haskell source code formatter
GNU Affero General Public License v3.0
690 stars 72 forks source link

Avoid pointless line wrapping #180

Open mightybyte opened 6 years ago

mightybyte commented 6 years ago

I just ran across a situation where this line:

    js "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/theme-solarized_dark.js"

...got wrapped to this:

    js
      "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/theme-solarized_dark.js"

This seems like the wrong thing to do. First of all, it's wrong because the wrapped version still exceeds the line length. Second, the wrapping only moves things left by a small amount. Even if the first reason wasn't the case, I think the second reason is still a valid reason for leaving a too-long line untouched.

This is a common issue in places where you have large string literals. Both of the above two criteria seem doable in terms of implementation feasibility, but additionally perhaps a third possible way of tackling this might be to just not try to wrap any line that contains a string literal longer than a certain length (say half the max line length or something similar).

Does anyone else have thoughts on this?

ElvishJerricco commented 6 years ago
  someReallyLongFunctionNameOrALongParenthesizedExpressionReturningAFunction "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/theme-solarized_dark.js"

should probably be formatted :P

  someReallyLongFunctionNameOrALongParenthesizedExpressionReturningAFunction
    "https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/theme-solarized_dark.js"

So for me it seems hard to come up with good hueristics here. Maybe don't put something on a new line if it's only going to be moved over by like one space, and otherwise remain unchanged? I dunno, that breaks a lot of formattings that I prefer...

lspitzner commented 6 years ago

This seems like the wrong thing to do. First of all, it's wrong because the wrapped version still exceeds the line length.

Taken to the extreme, this view would mean that any function containing a literal longer than 78/80 characters (or any such identifier) could (and should) be layouted as a single, (however drastically) overflowing line. The argument "but do-notation should remain multiple-line" would only weaken this from "any function" down to "any statement".

And with that in mind, the next step is indeed to look for a less drastic heuristic.

I think it might boil down to something along the lines of "if the last child of any syntax construct is drastically long, layout the whole thing as if it was short". But I fear that rule alone would have several annoying false positives.

mightybyte commented 6 years ago

@ElvishJerricco My heuristic of not wrapping if something is moved less than a certain amount to the left would handle your case.

@lspitzner One heuristic I have been thinking about is text "density"...in other words the percentage of non-whitespace characters visible on the screen. I don't know that this density metric should be universally maximized, but in the line wrapping situations I've thought brittany got wrong, it usually is reducing the density. All else being equal, the reformatting I encountered above significantly reduced the density. But for ElvishJerricco's suggested reformatting it would have increased the density. So this notion of density seems to be working decently thus far.

Consider another reformatting that brittany recently generated for me:

    elClass "div" "ui fluid action input" $ mdo
      name <- textInput $ def
          & textInputConfig_value .~ SetValue "" (Just $ "" <$ clicked)
          & textInputConfig_placeholder .~ pure "Enter key name"

...got reformatted to:

  elClass "div" "ui fluid action input" $ mdo
    name <-
      textInput
      $ def
      & textInputConfig_value
      .~ SetValue "" (Just $ "" <$ clicked)
      & textInputConfig_placeholder
      .~ pure "Enter key name"

This is substantially worse because none of the original lines exceeded the line length requirement. This particular example currently makes the use of brittany unacceptable for my project. Is there something we can do about this? Perhaps a config option that prevents brittany from reformatting lines shorter than the line length?

This is another example where brittany reduces the information density pretty significantly.

mightybyte commented 6 years ago

Here's another example where brittany's line wrapping is egregiously bad. This:

    onNewKey <- performEvent $ createKey signingKeys <$>
      -- Filter out keys with empty names
      ffilter (/= "") (conf ^. walletCfg_onRequestNewKey)

...got reformatted to:

    onNewKey <- performEvent $ createKey signingKeys <$>
      -- Filter out keys with empty names
                                                           ffilter
     (/= "")
     (conf ^. walletCfg_onRequestNewKey)
lspitzner commented 6 years ago

For the last two mentioned cases, issues #79 and #146 are very relevant. I will probably implement what @chreekat suggests in #146 via a new config option, although I still have to look into how finely grained this will be (and perhaps if there is a reason to change default config too, although that touches on a different topic..), also i think i am up to two unrelated issues that surface once you do that.. I will follow up on this in #146.

I think it is best to focus on these issues, as their resolution as i imagine it is in line with "visible screenspace density optimization", and probably resolves this part of this issue.

And even if it is a nice concept, an (admittedly brief) consideration does not let me belief that there are simple changes to the code possible to implement it. You could go encode a "density" attribute in the spacing information used in the internal algorithm, but that is a large-scope change, with questionable effectiveness - because supporting overflowing spacings would eliminate an important means for pruning the search space, so the result might be significantly less performant or yield worse results.