Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust indent block operations used for VB smart indent in argument lists #25344

Merged
merged 3 commits into from
Mar 9, 2018

Conversation

DustinCampbell
Copy link
Member

Fixes #25323

Customer scenario

Visual Basic argument lists (and other parenthesized lists, like parameter lists and type parameter lists),
have specialized smart indent behavior. Essentially, arguments after the first argument in a list are
indented to the same position as the first argument. Consider the following code.

Sub M()
    Console.WriteLine("{0} + {1} = {2}", 19, 23, 19 + 23)
End Sub

If the user presses ENTER before the second argument (19), the indentation looks like so.

Sub M()
    Console.WriteLine("{0} + {1} = {2}",
                      19, 23, 19 + 23)
End Sub

However, this can cause indentation issues if the user wants to ident all subsequent arguments differently. Consider the following scenario.

Sub M()
    Console.WriteLine("{0} + {1} = {2}",
        19, 23,
        19 + 23)
End Sub

Now, if the user presses ENTER before the third argument (23), the indentation of the first argument will be used, even though the user had indented the second argument differently:

Sub M()
    Console.WriteLine("{0} + {1} = {2}",
        19,
                      23,
        19 + 23)
End Sub

This problem occurs because a single indent block operation is created for the whole argument list using the indentation of the first argument. This change fixes the issue by creating multiple indent block operations using the indentation of the first argument, and the indentation of any argument that appears on a different line.

Bugs this fixes

Fixes #25323

Workarounds, if any

If the user wishes to format their argument lists in the manner described above, there really isn't a good way to workaround the issue. It's an annoyance and the editor will fight against the user's indentation.

Risk

Low. This change is localized to indentation in argument lists.

Performance impact

Low performance impact. Any new allocations do not happen in hot paths -- only when the user presses enter in an argument list.

Is this a regression from a previous update?

No.

Root cause analysis

I'm surprised we have not heard about this. This problem shows up in very simple cases where a method call has a large number of arguments. However, it is somewhat less common because many users will move the first argument to the next line and indent all arguments together at once.

How was the bug found?

I was reformatting the VB classification unit tests and found this issue to be particularly frustrating.

…ists

Visual Basic argument lists (and other parenthesized lists, like parameter lists and type parameter lists),
have specialized smart indent behavior. Essentially, arguments after the first argument in a list are
indented to the same position as the first argument. Consider the following code.

``` VB
Sub M()
    Console.WriteLine("{0} + {1} = {2}", 19, 23, 19 + 23)
End Sub
```

If the user presses ENTER before the second argument (19), the indentation looks like so.

``` VB
Sub M()
    Console.WriteLine("{0} + {1} = {2}",
                      19, 23, 19 + 23)
End Sub
```

However, this can cause indentation issues if the user wants to ident all subsequent arguments differently.
Consider the following scenario.

``` VB
Sub M()
    Console.WriteLine("{0} + {1} = {2}",
        19, 23,
        19 + 23)
End Sub
```

Now, if the user presses ENTER before the third argument (23), the indentation of the first argument will be
used, even though the user had indented the second argument differently:

``` VB
Sub M()
    Console.WriteLine("{0} + {1} = {2}",
        19,
                      23,
        19 + 23)
End Sub
```

This problem occurs because a single indent block operation is created for the whole argument list using the
indentation of the first argument. This change fixes the issue by creating multiple indent block operations
using the indentation of the first argument, and the indentation of any argument that appears on a different
line.
@DustinCampbell DustinCampbell requested a review from a team as a code owner March 8, 2018 15:40
@jinujoseph jinujoseph added this to the 15.7 milestone Mar 8, 2018
Copy link
Member

@sharwell sharwell left a comment

Choose a reason for hiding this comment

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

Needs tests for additional edge case, if only to demonstrate current behavior.

For Each argument In argumentList.Arguments
Dim lineNumber = sourceText.Lines.GetLineFromPosition(argument.SpanStart).LineNumber
If lineNumber > previousLineNumber Then
arguments.Add(argument)
Copy link
Member

@sharwell sharwell Mar 8, 2018

Choose a reason for hiding this comment

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

❓ What happens if the argument is split across lines, but doesn't actually start a line?

Method(a +
  b, c +
  d) ' <-- What happens if you add , and hit enter here?

Copy link
Contributor

@heejaechang heejaechang Mar 8, 2018

Choose a reason for hiding this comment

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

I am fine to be better on smart indent on expression. but just as FYI.

both C# and VB never tries to be too smart on multi line expressions. every one has different liking on those on every different cases even for same construct. (that's why formatter never force formatting on expressions)

we do have some special cases like argument or parameter lists, but even for those, we did very simple thing. (so that it can be predictable)

most of time, if expression is more than 2 lines a part, we just give up and act like simple block indent (either align with previous line or 4 space added after previous line)

Copy link
Contributor

@heejaechang heejaechang Mar 8, 2018

Choose a reason for hiding this comment

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

one thing I hope we don't do is, we forcing people to follow our liking and make them to demand options for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! Good question. I'll add a test or two to cover this scenario.

Copy link
Member Author

@DustinCampbell DustinCampbell Mar 8, 2018

Choose a reason for hiding this comment

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

@heejaechang: We aren't forcing people to follow our liking with this change and no options would ever be required. Instead, we're treating the special case of argument lists (which is different between C# and VB) to be a bit more natural. IOW, this should better match the user's expectations.

Class C
Sub M()
Console.WriteLine(""{0} + {1}"",
19, 23 +
Copy link
Member

@sharwell sharwell Mar 8, 2018

Choose a reason for hiding this comment

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

📝 This test doesn't cover the case I was describing because 19 is an argument that starts a line. 23 + gets excluded because it is on the same line as 19. I'm looking for a test case where an argument meets both of the following conditions:

  1. The argument is the first argument that starts on the line
  2. The first token of the argument is not the first token on the line

📝 My case isn't as contrives as you'd think. Consider the following:

Method(
    CallArgument1(
        x,
        y,
        z
    ), Argument2, ' <-- Press enter here

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I missed the first + in your original example. Will update.

Copy link
Member

Choose a reason for hiding this comment

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

No problem, the additional ones you did add were good tests too

Copy link
Member Author

Choose a reason for hiding this comment

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

And FWIW, I would expect that it would indent to the second argument in this case. I don't think that's necessarily correct, but it's a pretty fishy scenario.

…on a line is not the first token and is split across multiple lines

End Sub
End Class"

AssertSmartIndent(
code,
indentationLine:=6,
expectedIndentation:=16)
expectedIndentation:=13)
Copy link
Member

Choose a reason for hiding this comment

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

💡 This is super weird IMO. It would be better to filter arguments to cases where they start on a line. If not in this pull request, then it would be good to file a bug to clean this up "eventually". 😄

Copy link
Member Author

@DustinCampbell DustinCampbell Mar 8, 2018

Choose a reason for hiding this comment

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

If I understand the system correctly, filtering to just arguments that start a line would be even less predictable. This would leave gaps in the argument list where there isn't an indent block operation. In those cases, the indenter would move up the tree to the next indent block operation, which would likely be some node above the argument list. Consider the following code where the user presses ENTER after d,:

Sub M()
    Method(a +
      b, c +
      d,

End Sub

If there isn't an indent block operation created for c + d, the indentation would be 4 because the indent block operation from Sub M() .. End Sub would be used.

@heejaechang, please correct me if I'm not understanding the code correctly.

Copy link
Member Author

@DustinCampbell DustinCampbell Mar 8, 2018

Choose a reason for hiding this comment

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

@sharwell: I think the right experience is to for the first line break after the first argument in an argument list to use the same indentation as the first argument. Then, subsequent line breaks would just use the previous line's indentation. However, I couldn't find a way to achieve that in this system. 😞

Copy link
Member

@sharwell sharwell Mar 8, 2018

Choose a reason for hiding this comment

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

Skipping arguments because they don't start a line would be equivalent to skipping trailing arguments on the same line. The indentation block from the previous (or first) argument would be used. For the case above, pressing Enter after d would indent to match a.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, you're correct. 😄 I think that'd still be a weird experience though.

Sub M()
    Method(a +
      b, c +
      d,
           |
End Sub

Copy link
Contributor

@heejaechang heejaechang Mar 9, 2018

Choose a reason for hiding this comment

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

smart indentation, first use formatting rule to find indentation, after that it uses simple indentation logic (well, it is bunch of special cases around construct, so logic is simple but code is messy..) to find out where to put caret.
(http://index/?leftProject=Microsoft.CodeAnalysis.EditorFeatures&leftSymbol=poqp1cwzffsy&rightProject=Microsoft.CodeAnalysis.CSharp.EditorFeatures&file=Formatting%5CIndentation%5CCSharpIndentationService.Indenter.cs&line=136)

the simple indentation logic does either following previous line or add 4 more spaces.

the special formatting rule for argument, parameter was added for VB since vb had this behavior where caret aligned to first argument/parameter in the list rather than following previous line indentation or 4 spaces.

ex)
void method(A b,
............| <= here

if one wants this kind of behavior
void method(A b,
....| <= here

one can just remove formatting rule, and let default smart indentation logic to take over.

if one wants to use formatting rule for it, you can use this
http://index/?query=FormattingRu&rightProject=Microsoft.CodeAnalysis.Workspaces&file=Formatting%5CRules%5COperations%5CIndentBlockOption.cs&line=16 and set something like "," as base token, but formatter was not designed to format expressions. it is designed to leave expression exactly as they were as much as possible, so I dont think there is enough options or knobs around expressions.

I think if behavior desired is just letting caret to be either aligned with previous line or 4 more spaces after previous line, it would be better just to let default smart indent behavior to take care of it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @heejaechang. I think the right behavior for VB probably is:

  1. Align with the first argument on the second line of an argument list.
  2. Align with the previous line for subsequent lines in an argument list.

If I'm understanding you, removing the current VB formatting rule wouldn't provide that experience, correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

yep. I think your change is general improvement around this area. I was just saying if we want to make it even better, we can tweak it a bit more.

@jinujoseph
Copy link
Contributor

@Pilchie for 15.7.P2 ask mode approval

@Pilchie
Copy link
Member

Pilchie commented Mar 9, 2018

Approved - thanks!

@DustinCampbell
Copy link
Member Author

Thanks @Pilchie! @heejaechang / @sharwell, I've filed #25371 to track further improvement in this area.

@DustinCampbell DustinCampbell merged commit 6d6fcc0 into dotnet:dev15.7.x Mar 9, 2018
@DustinCampbell DustinCampbell deleted the issue-25323 branch August 24, 2021 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants