Fix SourceText.GetChanges exception on merging certain changes#39258
Fix SourceText.GetChanges exception on merging certain changes#39258RikkiGibson merged 4 commits intodotnet:masterfrom
Conversation
| AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength + newChange.NewLength - newChange.Span.Length)); | ||
| oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength - newChange.Span.Length; | ||
| oldIndex++; | ||
| newIndex++; |
There was a problem hiding this comment.
not certain why this is safe/ok. if newChange and oldChange have different contents, then what's going on here?
There was a problem hiding this comment.
It actually merges the list of TextChangeRange objects, not TextChange objects.
Those don't have contents, just old length and new length, so there is no difference in contents.
The ranges are mapped to the final text later, in GetTextChanges.
There was a problem hiding this comment.
I don't know if this will clarify things or not, but here's the way I understand this method:
- We have an original text
t1and callt1.WithTextChanges(oldChanges). This produces a ChangedTextt2, storing theoldChangesandt1in fields. - When we call
t2.WithTextChanges(newChanges)it produces a new ChangedTextt3, storing thenewChangesandt2. - When we call
t3.GetTextChanges(t1), we have to rewrite thenewChangesto make them occur w.r.t.t1instead of w.r.t.t2:- we have to split individual changes apart when old and new changes overlap with each other
- we merge adjacent changes together as we go to simplify them.
In the end we get a TextChangeRange[] which describes the difference between the original text and the final text.
| oldDelta = oldDelta - oldChange.Span.Length + oldChangeLeadingInsertion; | ||
| oldChange = new TextChangeRange(new TextSpan(oldChange.Span.Start, 0), oldChange.NewLength - oldChangeLeadingInsertion); | ||
| newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length), newChange.NewLength); | ||
| var oldChangeLeadingDeletion = Math.Min(oldChange.Span.Length, oldChangeLeadingInsertion); |
There was a problem hiding this comment.
OK, I'm talking myself through why this makes sense.
In this line we are figuring out how many characters of the old change occur before the new change. We assume that if the old change deletes characters then it will delete from the left at most as many as it is inserting on the left. When the old change's deletion is longer than the old change's leading insertion, the rest of its deletion has to be shifted over to remove the characters after the new change's insertion. #Resolved
| Assert.Equal("0aa4", change1.ToString()); | ||
| Assert.Equal("0bba4", change2.ToString()); | ||
| Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bba") }, changes); | ||
| } |
There was a problem hiding this comment.
Are these all cases that would have thrown before your change?
BTW, this new form for the tests is significantly easier to reason about, thanks.
There was a problem hiding this comment.
Only 3 of them would throw, but I no longer remember which ones. TestMergeChanges_Overlapping_NewInsideOld_AndOldHasDeletion for sure. We can revert the class itself on local and run tests to see.
There was a problem hiding this comment.
I ended up pulling down the changes after I commented to see what would break, and it was pretty much as you describe :)
I actually needed to pull down the changes and run the whole test class with the debugger with breakpoints on areas you changed to be sure I understood what the code was doing.
There was a problem hiding this comment.
Thanks for your time! Yep it was a bit hard to follow for me as well, existing tests were extremely useful.
RikkiGibson
left a comment
There was a problem hiding this comment.
LGTM, just had a few questions and suggestions
Co-Authored-By: Rikki Gibson <rikkigibson@gmail.com>
Co-Authored-By: Rikki Gibson <rikkigibson@gmail.com>
|
Could we please get a senior review on this? @dotnet/roslyn-compiler |
|
Ping @dotnet/roslyn-compiler |
| Assert.Equal("Cool ", changes[0].NewText); | ||
| } | ||
|
|
||
| [Fact] |
There was a problem hiding this comment.
Fact [](start = 9, length = 4)
Please add a WorkItem attribute to each of these three tests and the last test.
There was a problem hiding this comment.
Looks like the link might have been auto-formatted, did you mean something like:
[WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")]There was a problem hiding this comment.
Sorry, it's a bit unclear which 3 we are talking about -- I'll add to all the ones that were failing before this fix.
| Assert.Equal("0aa4", change1.ToString()); | ||
| Assert.Equal("0bba4", change2.ToString()); | ||
| Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bba") }, changes); | ||
| } |
There was a problem hiding this comment.
} [](start = 8, length = 1)
Do we also need to handle the case where the new deletion is larger than the old? It looks like the following fails with or without this PR:
var original = SourceText.From("01234");
var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa"));
var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 3), "bb"));
var changes = change2.GetTextChanges(original);
Assert.Equal("0aa4", change1.ToString());
Assert.Equal("0bb", change2.ToString());We can log a bug for this case and handle it separately.
There was a problem hiding this comment.
Thanks! I'll take a look in a few moments -- if it's easy to fix then maybe just fix it here, if hard (needs a lot of review) then I can do another PR.
There was a problem hiding this comment.
Seems similar to what I am already doing in newChange.Span.Start > oldChange.Span.Start + oldDelta, but that means it's complicated enough for it's own review.
So I would prefer to do as a separate bug/PR just to make review easier (also maybe there is some reuse that can be done between those two cases).
|
Thanks so much for your contribution @ashmind! |
Closes #22289
Closes #26305
See #22289 for full details on the bug.
Summary
After certain
WithChangescalls,SourceText.GetChangesis no longer able to handle theSourceTextstate and throwsArgumentOutOfRangeExceptionwhen called.Notes