Multiliner icon indicating copy to clipboard operation
Multiliner copied to clipboard

Syntax aware formatting

Open aheze opened this issue 2 years ago • 8 comments

Currently when you select a chunk of code that contains parameters nested inside, formatting is weird:

Memberwise init generated by Xcode

This is because Multiliner splits parameters by commas , — this includes commas inside nested inits.

https://github.com/aheze/Multiliner/blob/150c4a99e37470c49eb3b2b6e7a6417946ba9f40/Sources/MultilinerExtension/SourceEditorCommand.swift#L120-L123

Possibly a split using Regex?

aheze avatar Jun 28 '22 04:06 aheze

Maybe you could parse the code into an AST using apple/swift-syntax to understand the code better. The resulting type information could be helpful to make more informed decisions on how to rewrite the code. (Maybe you could also use this to identify the initial range?)

calimarkus avatar Jul 02 '22 08:07 calimarkus

Worth a try. Not sure how the performance will be impacted though

aheze avatar Jul 08 '22 20:07 aheze

It's designed for "performance-critical applications" - whatever that may mean:

SwiftSyntax is a set of Swift bindings for the libSyntax library. [...] Its API is designed for performance-critical applications.

calimarkus avatar Jul 08 '22 21:07 calimarkus

Right. Thanks for the suggestion, I'll do some experimenting this week!

aheze avatar Jul 09 '22 02:07 aheze

Yeah, because of assumptions the parsing makes, it can format things in an unexpected way so you have to be fairly specific about what you highlight sometimes. I find myself needing to hit CtrlI afterward which then indents the results correctly.

Adopting swift-syntax would be great - could help with the heavy lifting of handling recursive and complex formatting, and would evolve as the Swift language evolves too.

It would be hugely beneficial to be adding unit tests to ensure formatting continues to work as expected as the extension is updated over time, since there's so many edge cases and variations in code formatting.

(I would be game for helping out with writing unit tests at some point.)

orchetect avatar Jul 09 '22 18:07 orchetect

One major benefit to using a formal AST is that you could format an entire file in one go. I often come across code I inherit or old code I wrote where parameters were all on single lines. It would be amazing to just CmdA and run the formatter.

I did a bit of snooping and it looks like some linters/formatters are already capable of this:

apple/swift-format:

  • lineBreakBeforeEachArgument (boolean): Determines the line-breaking behavior for generic arguments and function arguments when a declaration is wrapped onto multiple lines. If true, a line break will be added before each argument, forcing the entire argument list to be laid out vertically. If false (the default), arguments will be laid out horizontally first, with line breaks only being fired when the line length would be exceeded.

  • lineBreakBeforeEachGenericRequirement (boolean): Determines the line-breaking behavior for generic requirements when the requirements list is wrapped onto multiple lines. If true, a line break will be added before each requirement, forcing the entire requirements list to be laid out vertically. If false (the default), requirements will be laid out horizontally first, with line breaks only being fired when the line length would be exceeded.

  • lineBreakAroundMultilineExpressionChainComponents (boolean): Determines whether line breaks should be forced before and after multiline components of dot-chained expressions, such as function calls and subscripts chained together through member access (i.e. "." expressions). When any component is multiline and this option is true, a line break is forced before the "." of the component and after the component's closing delimiter (i.e. right paren, right bracket, right brace, etc.).

SwiftFormat:

--wraparguments before-first

- foo(bar: Int,
-     baz: String)

+ foo(
+   bar: Int,
+   baz: String
+ )
- class Foo<Bar,
-           Baz>

+ class Foo<
+   Bar,
+   Baz
+ >

--wrapparameters after-first

- func foo(
-   bar: Int,
-   baz: String
- ) {
    ...
  }

+ func foo(bar: Int,
+          baz: String) {
    ...
  }

--wrapcollections before-first:

- let foo = [bar,
             baz,
-            quuz]

+ let foo = [
+   bar,
    baz,
+   quuz
+ ]

orchetect avatar Jul 24 '22 05:07 orchetect

@orchetect ah, so both SwiftFormats can kind of replace Multiliner. Nice investigating!

I guess if you want full file formatting it's better to go with one of the above. Multiliner was originally meant to be a quick shortcut to expand whatever line you're currently on. This makes it pretty fast actually (since it doesn't need to parse the whole file, it formats instantly even if your file is huge).

I've thought about whole file formatting, but the SwiftFormats can do this already and are great at it. Maybe it would be better to keep Multiliner lightweight and focus more on small shortcuts. I'll still look into AST though

aheze avatar Jul 24 '22 15:07 aheze

I don't think it's a replacement necessarily, after testing them out. It's also not exactly the same as Multiliner, you have to tweak the rules to get them to force it and it only works under certain circumstances. Whereas Multiliner does it every time. So I'd still probably use both depending on circumstances.

What I was thinking is it might be possible to learn from their source code how they're doing it.

orchetect avatar Jul 24 '22 17:07 orchetect