Skip to content

Provide a better experience editing a file with merge-markers in it.#15850

Merged
CyrusNajmabadi merged 10 commits intodotnet:masterfrom
CyrusNajmabadi:mergeMarkers
Mar 1, 2017
Merged

Provide a better experience editing a file with merge-markers in it.#15850
CyrusNajmabadi merged 10 commits intodotnet:masterfrom
CyrusNajmabadi:mergeMarkers

Conversation

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Dec 12, 2016

Fixes: #5028

This is a port of an extremely popular feature we did in TypeScript.

Today, if you try to edit a file with git merge-markers in it, you get a pretty terrible experience. Because of the text like <<<<<< in the file, the parser gets very thrown off, and that impacts all subsequent features.

To alleviate that, we augmented the TypeScript lexer to understand and be resilient to these merge markers, effectively making them part of our syntax model. Here's what things look like with the same feature applied to C#:

image

Note the clear errors indicating that we detected merge-conflicts.

In this system the higher levels then treat the merge-conflicts effectively as comments. So we classify them as comments, and we better support the code between them. In fact, in the 'top' part of the merge, you get a full C# experience (i.e. completion, highlight references, formatting, etc.). In the bottom part, we lexically-classify, but that's about it. In effect, it's treated similarly to an 'excluded code' region, except that we lexically classify since we're much more certain that is contains code.

--

A future feature i would lke to add would be a lightbulb/code-action that then easily let's you say "pick mine/theirs" to resolve merge conflicts :)

@CyrusNajmabadi
Copy link
Contributor Author

Tagging @dotnet/roslyn-compiler @dotnet/roslyn-ide .

This is a new feature, and i don't think meets the bar for VS2017. However, this is a super nice thing to add and it makes the experience around editing merge-conflict files so much nicer than today. I would like to get this into post-dev15.

Note: tests are forthcoming. I just wanted to get the initial code in so people could take a look. This code is really a straight port of the exact same feature we did in TypeScript several years back.

@gafter gafter added this to the 2.1 milestone Dec 12, 2016
@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Dec 13, 2016

I also added this feature to make working with merge conflicts nicer: #15853

image

@CyrusNajmabadi
Copy link
Contributor Author

I have done the VB side of this as well. So i would appreciate VB and C# eyes on this.

@jcouv jcouv self-assigned this Dec 29, 2016
Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax.Type.get -> Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax.WithIdentifier(Microsoft.CodeAnalysis.SyntaxToken identifier) -> Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax.Update(Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax type, Microsoft.CodeAnalysis.SyntaxToken identifier) -> Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax.WithIdentifier(Microsoft.CodeAnalysis.SyntaxToken identifier) -> Microsoft.CodeAnalysis.CSharp.Syntax.TupleElementSyntax
Copy link
Member

Choose a reason for hiding this comment

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

FYI @jaredpar @gafter for public API change.


// All conflict markers consist of the same character repeated seven times. If it is
// a <<<<<<< or >>>>>>> marker then it is also followed by a space.
private static readonly int s_conflictMarkerLength = "<<<<<<<".Length;
Copy link
Member

Choose a reason for hiding this comment

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

... = 7; seems simpler, especially since you have a comment already ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I far prefer the "string".Length pattern. The code is then self-describing and good, regardless of the state of the comment.

var text = TextWindow.Text;
if (position == 0 || SyntaxFacts.IsNewLine(text[position - 1]))
{
var firstCh = text[position];
Copy link
Member

Choose a reason for hiding this comment

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

firstCh [](start = 20, length = 7)

Maybe check or assert that firstCh is one of '<', '>' or '='?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i can do that.

var startCh = this.TextWindow.PeekChar();

// First create a trivia from the start of this merge conflict marker to the
// end of line/file (whicever comes first).
Copy link
Member

Choose a reason for hiding this comment

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

whicever [](start = 33, length = 8)

s/whicever/whichever/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure.

@agocke agocke changed the base branch from post-dev15 to master February 14, 2017 23:28
@CyrusNajmabadi
Copy link
Contributor Author

Anything else @jcouv

@jaredpar
Copy link
Member

We need to chat before merging this. It's essentially adding a feature to the compiler layer. That means it's putting us on the hook for future bug fixes, etc ...

@CyrusNajmabadi
Copy link
Contributor Author

CyrusNajmabadi commented Feb 17, 2017

Sure. We can talk about this. I can def see how this is a feature (since there are API changes for this). However, the core idea here is that this is just better error recovery. Right now our error recovery is basically horrendous/non-existent. Leading to a fairly terrible user experience in a reasonably common editing case. As this is really just about error-recovery, we always reserve the right to change how we behave here (including removing all this code entirely if we want). We've always asserted that error files may change in terms of how our APIs present them (including at the syntactic level).

Also, from doing this in TS, the long term cost is effectively nothing. AFAIK there hasn't ever even been a single bug that needed to get fixed here. So, while it is a feature, it's an extremely low cost one. I'm hoping the bang/buck equation makes this an easy choice.

--

Also, from a selfish Roslyn perspective, i think this is quite helpful to the team. Working with unmerged files today is basically extremely unpleasant in the IDE. You really have no choice but to treat the file as a blob of text that you manipulate roughly, while giving up most features. This tiny feature suddenly makes it completely reasonable and effective to work with these files jsut as you would any other C#/VB file. This turned out to be quite nice for TS development when this was added.

break;
}

if (ch == '>' && IsConflictMarkerTrivia())
Copy link
Member

@gafter gafter Feb 17, 2017

Choose a reason for hiding this comment

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

Do you want to accept conflict markers that are anywhere but the beginning of a line? #Resolved

Copy link
Member

@gafter gafter Feb 17, 2017

Choose a reason for hiding this comment

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

Ah, I see that is tested by IsConflictMarkerTrivia. #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IsConflictMarkerTrivia actually verifies that it's at the start of a line. Which is pretty much all that we want since htat's what you always get from Git.

@CyrusNajmabadi
Copy link
Contributor Author

Also worth noting: this is effectively a straight port of the TS code I wrote for this.

ERR_PublicSignNetModule = 37282
ERR_BadAssemblyName = 37283

ERR_Merge_conflict_marker_encountered = 37282
Copy link
Contributor Author

Choose a reason for hiding this comment

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

clearly this needs to be updated.

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

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

We've discussed with @Pilchie and @CyrusNajmabadi. This is a feature the IDE will own within the compiler.

@CyrusNajmabadi
Copy link
Contributor Author

@jaredpar Any other concerns you have? Or is this ready for review? Tnx!

@jaredpar
Copy link
Member

@CyrusNajmabadi i'm always concerned 😄

Fine for review.

@CyrusNajmabadi
Copy link
Contributor Author

Great :) Glad to work toward making you bald like me.

@dotnet/roslyn-compiler Please review when convenient (though ideally sooner rather than later) :)

@CyrusNajmabadi
Copy link
Contributor Author

@dotnet/roslyn-ide Please review IDE sections as well. Thanks!


// All conflict markers consist of the same character repeated seven times. If it is
// a <<<<<<< or >>>>>>> marker then it is also followed by a space.
private static readonly int s_conflictMarkerLength = "<<<<<<<".Length;
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: I'd personally make the "<<<<<<<" a named constant. eg s_conflictMarker`

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not certain what that buys. It's a value only used in a single place. Why introduce an indirection when it is already self-describing?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thought it was used else where, if it was it would make sense to share the content. So it had the same content.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks

@CyrusNajmabadi
Copy link
Contributor Author

@gafter can you take a look?

@dotnet/roslyn-ide please review IDE parts.

return;
}

case '>':
Copy link
Member

Choose a reason for hiding this comment

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

case [](start = 20, length = 4)

A valid program can have >>>>>>> at the beginning of a line. e.g.

List<List<List<List<List<List<List<int
>>>>>>> 
x = null;

Are we intentionally breaking this "scenario"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. We would be breaking that scenario. Up to you guys if you think such a break could realistically happen. Note: the issue occurs for TS, and we never got a single report about that.

Copy link
Member

Choose a reason for hiding this comment

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

How about this idea: we stop recognizing >>>>>>> as a conflict marker, but continue having >>>>>>> end a skipped text section that is started with a =======. I think that would remove the compat break and still retain all the value of the feature. Does that work?


In reply to: 103549556 [](ancestors = 103549556)

Copy link
Member

Choose a reason for hiding this comment

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

To clarify, the idea is that the skipped text section would be recognized by the scanner.


In reply to: 103551073 [](ancestors = 103551073,103549556)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. I think that is doable. Trying this out now.

@CyrusNajmabadi
Copy link
Contributor Author

@gafter Checking to see how feasible that would be.

@CyrusNajmabadi
Copy link
Contributor Author

@gafter I have made that change (and added a test for the large-generic parsing case as well).

@AdamSpeight2008
Copy link
Contributor

@CyrusNajmabadi What happens it the "markers" are not valid markers but actually text.

"
=======

>>>>>>> 
"

@CyrusNajmabadi
Copy link
Contributor Author

@AdamSpeight2008 Nothing happens. This is about lexing trivia. The contents of a string are not trivia and are consumed by the normal string lexing routine.

void M()
{
var v = new List<List<List<List<List<List<List<int
>>>>>>>
Copy link
Member

Choose a reason for hiding this comment

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

Please add a space after the last >.

@AdamSpeight2008
Copy link
Contributor

@CyrusNajmabadi Thank-You for clarifying, I'm still learning.

Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

This is great, thanks!

@CyrusNajmabadi
Copy link
Contributor Author

@AdamSpeight2008 We all are :) Glad to help.

@CyrusNajmabadi
Copy link
Contributor Author

@gafter Will make that change and merge in.

@CyrusNajmabadi
Copy link
Contributor Author

@dotnet/roslyn-ide Still need reviewers for the IDE sections.

Copy link
Contributor

@dpoeschl dpoeschl left a comment

Choose a reason for hiding this comment

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

IDE portions look good.

}

[Fact, Trait(Traits.Feature, Traits.Features.Classification)]
public async Task TestConflictMarkers1()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want a test here for the really, really generic case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I can add that. It's more a test that nothing goes horribly wrong (like crashing/index-out-of-bounds) vs actually verifying that we classify correctly.


Private Sub ClassifyDisabledText(trivia As SyntaxTrivia, triviaList As SyntaxTriviaList)
Dim index = triviaList.IndexOf(trivia)
If index >= 2 AndAlso
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to share this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly now. It used to be not possible due to the different syntax models. Now that we've unified, tihs area is ripe for an overhaul. I'll add to my cleanup backlog.

@CyrusNajmabadi CyrusNajmabadi merged commit 1b0cf5c into dotnet:master Mar 1, 2017
@CyrusNajmabadi CyrusNajmabadi deleted the mergeMarkers branch March 1, 2017 22:59
ERR_ExpressionTreeContainsDiscard = 8207,
#endregion more stragglers for C# 7

ERR_Merge_conflict_marker_encountered = 8300,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use mixed case: ERR_MergeConflictMarkerEncountered

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.

Roslyn C#/VB should detect Git-style merge conflicts and be resilient to them in editing scenarios.

9 participants