//************************************************************************* /// <summary> /// Concatentates the two input DiffChange lists and returns the resulting /// list. /// </summary> /// <param name="left">The left changes</param> /// <param name="right">The right changes</param> /// <returns>The concatenated list</returns> //************************************************************************* private IDiffChange[] ConcatenateChanges(IDiffChange[] left, IDiffChange[] right) { IDiffChange mergedChange; if (left.Length == 0 || right.Length == 0) { return((right.Length > 0) ? right : left); } else if (ChangesOverlap(left[left.Length - 1], right[0], out mergedChange)) { // Since we break the problem down recursively, it is possible that we // might recurse in the middle of a change thereby splitting it into // two changes. Here in the combining stage, we detect and fuse those // changes back together IDiffChange[] result = new IDiffChange[left.Length + right.Length - 1]; Array.Copy(left, 0, result, 0, left.Length - 1); result[left.Length - 1] = mergedChange; Array.Copy(right, 1, result, left.Length, right.Length - 1); return(result); } else { IDiffChange[] result = new IDiffChange[left.Length + right.Length]; Array.Copy(left, 0, result, 0, left.Length); Array.Copy(right, 0, result, left.Length, right.Length); return(result); } }
/// <summary> /// This methods combines two <see cref="IDiffChange"/> objects into one. /// </summary> /// <param name="diffChange">The diff change to add.</param> /// <returns>A new instance of <see cref="IDiffChange"/> that represents <c>this</c> + <see cref="diffChange"/>.</returns> public IDiffChange Add(IDiffChange diffChange) { if (diffChange == null) return this; int originalStart = Math.Min(OriginalStart, diffChange.OriginalStart); int originalEnd = Math.Max(OriginalEnd, diffChange.OriginalEnd); int modifiedStart = Math.Min(ModifiedStart, diffChange.ModifiedStart); int modifiedEnd = Math.Max(ModifiedEnd, diffChange.ModifiedEnd); DiffChangeType changeType; if (ChangeType == diffChange.ChangeType && OriginalStart - diffChange.OriginalEnd == 0 && ModifiedStart - diffChange.ModifiedEnd == 0) changeType = ChangeType; else changeType = DiffChangeType.Change; return new DiffChange { ChangeType = changeType, OriginalStart = originalStart, OriginalEnd = originalEnd, OriginalLength = originalEnd - originalStart, ModifiedStart = modifiedStart, ModifiedEnd = modifiedEnd, ModifiedLength = modifiedEnd - modifiedStart }; }
//************************************************************************* /// <summary> /// Returns true if the two changes overlap and can be merged into a single /// change /// </summary> /// <param name="left">The left change</param> /// <param name="right">The right change</param> /// <param name="mergedChange">The merged change if the two overlap, /// null otherwise</param> /// <returns>True if the two changes overlap</returns> //************************************************************************* private bool ChangesOverlap(IDiffChange left, IDiffChange right, out IDiffChange mergedChange) { Debug.Assert(left.OriginalStart <= right.OriginalStart, "Left change is not less than or equal to right change"); Debug.Assert(left.ModifiedStart <= right.ModifiedStart, "Left change is not less than or equal to right change"); if (left.OriginalStart + left.OriginalLength >= right.OriginalStart || left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart) { int originalStart = left.OriginalStart; int originalLength = left.OriginalLength; int modifiedStart = left.ModifiedStart; int modifiedLength = left.ModifiedLength; if (left.OriginalStart + left.OriginalLength >= right.OriginalStart) { originalLength = right.OriginalStart + right.OriginalLength - left.OriginalStart; } if (left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart) { modifiedLength = right.ModifiedStart + right.ModifiedLength - left.ModifiedStart; } mergedChange = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength); return(true); } else { mergedChange = null; return(false); } }
/// <summary> /// This methods combines two <see cref="IDiffChange"/> objects into one. /// </summary> /// <param name="diffChange">The diff change to add.</param> /// <returns>A new instance of <see cref="IDiffChange"/> that represents <c>this</c> + <see cref="diffChange"/>.</returns> public IDiffChange Add(IDiffChange diffChange) { if (diffChange == null) { return(this); } int originalStart = Math.Min(OriginalStart, diffChange.OriginalStart); int originalEnd = Math.Max(OriginalEnd, diffChange.OriginalEnd); int modifiedStart = Math.Min(ModifiedStart, diffChange.ModifiedStart); int modifiedEnd = Math.Max(ModifiedEnd, diffChange.ModifiedEnd); DiffChangeType changeType; if (ChangeType == diffChange.ChangeType && OriginalStart - diffChange.OriginalEnd == 0 && ModifiedStart - diffChange.ModifiedEnd == 0) { changeType = ChangeType; } else { changeType = DiffChangeType.Change; } return(new DiffChange { ChangeType = changeType, OriginalStart = originalStart, OriginalEnd = originalEnd, OriginalLength = originalEnd - originalStart, ModifiedStart = modifiedStart, ModifiedEnd = modifiedEnd, ModifiedLength = modifiedEnd - modifiedStart }); }
/// <summary> /// Get a committed text from the original sequence of the <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> /// <param name="removeLastLineTerminator">Remove line terminator for the last line of the returned text, if line terminator is present.</param> /// <returns>Original text from the <see cref="IDiffChange"/>.</returns> public string GetOriginalText(IDiffChange diffChange, bool removeLastLineTerminator) { if (diffChange.ChangeType == DiffChangeType.Insert) { return(string.Empty); } return(GetOriginalText(diffChange.OriginalStart, diffChange.OriginalEnd, removeLastLineTerminator)); }
/// <summary> /// Get a local text from the modified sequence of the <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> /// <param name="removeLastLineTerminator">Remove line terminator for the last line of the returned text, if line terminator is present.</param> /// <returns>Modified text from the <see cref="IDiffChange"/>.</returns> public string GetModifiedText(IDiffChange diffChange, bool removeLastLineTerminator) { if (diffChange.ChangeType == DiffChangeType.Delete) { return(string.Empty); } return(GetModifiedText(diffChange.ModifiedStart, diffChange.ModifiedEnd, removeLastLineTerminator)); }
/// <summary> /// Rollback all modified text in the document except specified <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void RollbackAllButThisChange(IDiffChange diffChange) { ITextEdit edit = _textView.TextBuffer.CreateEdit(); Span viewSpan; try { string modifiedRegionText = _marginCore.GetModifiedText(diffChange, false); string originalText = _marginCore.GetOriginalText(); edit.Delete(0, edit.Snapshot.Length); edit.Insert(0, originalText); ApplyEdit(edit, "Undo Modified Text"); edit = _textView.TextBuffer.CreateEdit(); ITextSnapshotLine startLine = edit.Snapshot.GetLineFromLineNumber(diffChange.OriginalStart); ITextSnapshotLine endLine = edit.Snapshot.GetLineFromLineNumber(diffChange.OriginalEnd); int start = startLine.Start.Position; int length = endLine.EndIncludingLineBreak.Position - start; switch (diffChange.ChangeType) { case DiffChangeType.Insert: edit.Insert(start, modifiedRegionText); viewSpan = new Span(start, modifiedRegionText.Length); break; case DiffChangeType.Delete: edit.Delete(start, length); viewSpan = new Span(start, 0); break; case DiffChangeType.Change: edit.Replace(start, length, modifiedRegionText); viewSpan = new Span(start, modifiedRegionText.Length); break; default: throw new InvalidEnumArgumentException(); } ApplyEdit(edit, "Restore Modified Region"); } catch (Exception) { edit.Cancel(); throw; } var viewSnapshotSpan = new SnapshotSpan(_textView.TextSnapshot, viewSpan); _textView.ViewScroller.EnsureSpanVisible(viewSnapshotSpan, EnsureSpanVisibleOptions.AlwaysCenter); }
/// <summary> /// Event handler that occurs when the left mouse button is released while the mouse pointer is over the margin element. /// </summary> /// <param name="sender">Event sender (the margin element).</param> /// <param name="args">Event arguments.</param> private void OnMarginElementMouseLeftButtonUp(object sender, MouseButtonEventArgs args) { var element = (FrameworkElement)sender; // Disable copying commited text if diff inserted (otherwise an empty string is always copied). dynamic data = element.Tag; IDiffChange diffChange = data.DiffChangeInfo; _copyMenuItem.IsEnabled = diffChange.ChangeType != DiffChangeType.Insert; _contextMenu.PlacementTarget = element; _contextMenu.IsOpen = true; args.Handled = true; }
/// <summary> /// Event handler that occurs when the mouse pointer enters the bounds of the margin element. /// </summary> /// <param name="sender">Event sender (the margin element).</param> /// <param name="args">Event arguments.</param> private void OnMarginElementMouseEnter(object sender, MouseEventArgs args) { var marginElement = (FrameworkElement)sender; dynamic data = marginElement.Tag; IDiffChange diffChange = data.DiffChangeInfo; if (marginElement.ToolTip == null) { ToolTipService.SetShowDuration(marginElement, 3600000); string text = _marginCore.GetOriginalText(diffChange, true); marginElement.ToolTip = text; // TODO: Если регион свернут, то тултип будет только для первого изменения в нем, т.к. ContainsChanges возвращает первую пересекаемую строку. // Все пересекаемые строки не учитываются. Нужно добавить поддержку всех пересекаемых строк, но только для показа тултипа, чтобы не замедлять рендеринг. } }
/// <summary> /// Add the changed line in the collection. /// </summary> /// <param name="line">A line of text from an <see cref="ITextSnapshot"/>.</param> /// <param name="diffChangeInfo">Information about the difference of line.</param> public void Add(ITextSnapshotLine line, IDiffChange diffChangeInfo) { ITextSnapshotLine existsLine = _diffDict.Keys.FirstOrDefault(x => x.LineNumber == line.LineNumber); if (existsLine != null) { IDiffChange oldDiffChangeInfo = _diffDict[existsLine]; diffChangeInfo = oldDiffChangeInfo.Add(diffChangeInfo); _dict[oldDiffChangeInfo.ChangeType].Remove(existsLine); _diffDict.Remove(existsLine); } _dict[diffChangeInfo.ChangeType].Add(line); _diffDict[line] = diffChangeInfo; }
/// <summary> /// This methods combines two DiffChange objects into one /// </summary> /// <param name="diffChange">The diff change to add</param> /// <returns>A IDiffChange object that represnets this + diffChange</returns> public IDiffChange Add(IDiffChange diffChange) { //If the diff change is null then just return this if (diffChange == null) { return(this); } int originalStart = Math.Min(this.OriginalStart, diffChange.OriginalStart); int originalEnd = Math.Max(this.OriginalEnd, diffChange.OriginalEnd); int modifiedStart = Math.Min(this.ModifiedStart, diffChange.ModifiedStart); int modifiedEnd = Math.Max(this.ModifiedEnd, diffChange.ModifiedEnd); return(new DiffChange(originalStart, originalEnd - originalStart, modifiedStart, modifiedEnd - modifiedStart)); }
/// <summary> /// Event handler that occurs when "Compare region with diff tool" menu item is clicked. /// </summary> /// <param name="sender">Event sender (the <see cref="MenuItem"/>).</param> /// <param name="routedEventArgs">Event arguments.</param> private void CompareChangeRegionMenuItemOnClick(object sender, RoutedEventArgs routedEventArgs) { try { var marginElement = (FrameworkElement)_contextMenu.PlacementTarget; dynamic data = marginElement.Tag; IDiffChange diffChange = data.DiffChangeInfo; CompareChangeRegion(diffChange); routedEventArgs.Handled = true; } catch (Exception ex) { ShowException(ex); } }
/// <summary> /// Rollback modified text. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void RollbackChange(IDiffChange diffChange) { ITextEdit edit = _textView.TextBuffer.CreateEdit(); try { ITextSnapshot snapshot = edit.Snapshot; ITextSnapshotLine startLine = snapshot.GetLineFromLineNumber(diffChange.ModifiedStart); ITextSnapshotLine endLine = snapshot.GetLineFromLineNumber(diffChange.ModifiedEnd); int start; if (diffChange.ChangeType != DiffChangeType.Delete) { start = startLine.Start.Position; int length = endLine.EndIncludingLineBreak.Position - start; edit.Delete(start, length); } else { if (startLine.LineNumber == 0 && endLine.LineNumber == 0) { start = startLine.Start.Position; } else { start = startLine.EndIncludingLineBreak.Position; } } if (diffChange.ChangeType != DiffChangeType.Insert) { string text = _marginCore.GetOriginalText(diffChange, false); edit.Insert(start, text); } ApplyEdit(edit, "Rollback Modified Region"); } catch (Exception) { edit.Cancel(); throw; } }
/// <summary> /// Creates a <see cref="DiffChange"/> based on the <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> public DiffChange(IDiffChange diffChange) { ChangeType = diffChange.ChangeType; OriginalStart = diffChange.OriginalStart; OriginalEnd = Math.Max(0, diffChange.OriginalEnd - 1); // exclusive bound to inclusive. OriginalLength = diffChange.OriginalLength; if (ChangeType == DiffChangeType.Delete) { ModifiedStart = Math.Max(0, diffChange.ModifiedEnd - 1); // exclusive upper bound. ModifiedEnd = Math.Max(0, diffChange.ModifiedStart - 1); // exclusive lower bound. } else { ModifiedStart = diffChange.ModifiedStart; ModifiedEnd = Math.Max(0, diffChange.ModifiedEnd - 1); // exclusive bound to inclusive. } ModifiedLength = diffChange.ModifiedLength; }
/// <summary> /// Copy commited text to the Clipboard. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void CopyCommitedText(IDiffChange diffChange) { string text = _marginCore.GetOriginalText(diffChange, true); Clipboard.SetText(text, TextDataFormat.UnicodeText); }
//************************************************************************* /// <summary> /// Computes the differences between the given original and modified sequences. /// </summary> /// <param name="original">The original sequence.</param> /// <param name="modified">The modified sequence.</param> /// <param name="elementComparer">The diff element comparer.</param> /// <returns>The set of differences between the sequences.</returns> //************************************************************************* public IDiffChange[] Diff(IList <T> original, IList <T> modified, IEqualityComparer <T> elementComparer, ContinueDifferencePredicate <T> predicate) { Debug.Assert(original != null, "original is null"); Debug.Assert(modified != null, "modified is null"); Debug.Assert(elementComparer != null, "elementComparer is null"); m_original = original; m_modified = modified; m_elementComparer = elementComparer; m_predicate = predicate; m_originalIds = new int[OriginalSequence.Count]; m_modifiedIds = new int[ModifiedSequence.Count]; int originalStart = 0; int originalEnd = OriginalSequence.Count - 1; int modifiedStart = 0; int modifiedEnd = ModifiedSequence.Count - 1; // Find the start of the differences while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && ElementsAreEqual(originalStart, modifiedStart)) { originalStart++; modifiedStart++; } // Find the end of the differences while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && ElementsAreEqual(originalEnd, modifiedEnd)) { originalEnd--; modifiedEnd--; } // In the very special case where one of the ranges is negative // Either the sequences are identical, or there is exactly one insertion // or there is exactly one deletion. We have no need to compute the diffs. if (originalStart > originalEnd || modifiedStart > modifiedEnd) { IDiffChange[] changes; if (modifiedStart <= modifiedEnd) { Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); // All insertions changes = new IDiffChange[1]; changes[0] = new DiffChange(originalStart, 0, modifiedStart, modifiedEnd - modifiedStart + 1); } else if (originalStart <= originalEnd) { Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // All deletions changes = new IDiffChange[1]; changes[0] = new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, 0); } else { Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // Identical sequences - No differences changes = new IDiffChange[0]; } return(changes); } // Now that we have our bounds, calculate unique ids for all // IDiffElements in the bounded range. That way, we speed up // element comparisons during the diff computation. ComputeUniqueIdentifiers(originalStart, originalEnd, modifiedStart, modifiedEnd); // Compute the diffences on the bounded range return(ComputeDiff(originalStart, originalEnd, modifiedStart, modifiedEnd)); }
/// <summary> /// Rollback modified text. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void RollbackChange(IDiffChange diffChange) { ITextEdit edit = _textView.TextBuffer.CreateEdit(); try { ITextSnapshot snapshot = edit.Snapshot; ITextSnapshotLine startLine = snapshot.GetLineFromLineNumber(diffChange.ModifiedStart); ITextSnapshotLine endLine = snapshot.GetLineFromLineNumber(diffChange.ModifiedEnd); int start; if (diffChange.ChangeType != DiffChangeType.Delete) { start = startLine.Start.Position; int length = endLine.EndIncludingLineBreak.Position - start; edit.Delete(start, length); } else { if (startLine.LineNumber == 0 && endLine.LineNumber == 0) start = startLine.Start.Position; else start = startLine.EndIncludingLineBreak.Position; } if (diffChange.ChangeType != DiffChangeType.Insert) { string text = _marginCore.GetOriginalText(diffChange, false); edit.Insert(start, text); } ApplyEdit(edit, "Rollback Modified Region"); } catch (Exception) { edit.Cancel(); throw; } }
//************************************************************************* /// <summary> /// Private helper method which computes the differences on the bounded range /// recursively. /// </summary> /// <returns>An array of the differences between the two input /// sequences.</returns> //************************************************************************* private IDiffChange[] ComputeDiffRecursive(int originalStart, int originalEnd, int modifiedStart, int modifiedEnd, out bool quitEarly) { quitEarly = false; // Find the start of the differences while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && ElementsAreEqual(originalStart, modifiedStart)) { originalStart++; modifiedStart++; } // Find the end of the differences while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && ElementsAreEqual(originalEnd, modifiedEnd)) { originalEnd--; modifiedEnd--; } // In the special case where we either have all insertions or all deletions or the sequences are identical if (originalStart > originalEnd || modifiedStart > modifiedEnd) { IDiffChange[] changes; if (modifiedStart <= modifiedEnd) { Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); // All insertions changes = new IDiffChange[1]; changes[0] = new DiffChange(originalStart, 0, modifiedStart, modifiedEnd - modifiedStart + 1); } else if (originalStart <= originalEnd) { Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // All deletions changes = new IDiffChange[1]; changes[0] = new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, 0); } else { Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); // Identical sequences - No differences changes = new IDiffChange[0]; } return(changes); } // This problem can be solved using the Divide-And-Conquer technique. int midOriginal, midModified; IDiffChange[] result = ComputeRecursionPoint(originalStart, originalEnd, modifiedStart, modifiedEnd, out midOriginal, out midModified, out quitEarly); if (result != null) { // Result is not-null when there was enough memory to compute the changes while // searching for the recursion point return(result); } else if (!quitEarly) { // We can break the problem down recursively by finding the changes in the // First Half: (originalStart, modifiedStart) to (midOriginal, midModified) // Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd) // NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point IDiffChange[] leftChanges = ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, out quitEarly); IDiffChange[] rightChanges = new IDiffChange[0]; if (!quitEarly) { rightChanges = ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd, out quitEarly); } else { // We did't have time to finish the first half, so we don't have time to compute this half. // Consider the entire rest of the sequence different. rightChanges = new DiffChange[] { new DiffChange(midOriginal + 1, originalEnd - (midOriginal + 1) + 1, midModified + 1, modifiedEnd - (midModified + 1) + 1) }; } return(ConcatenateChanges(leftChanges, rightChanges)); } // If we hit here, we quit early, and so can't return anything meaningful return(new DiffChange[] { new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, modifiedEnd - modifiedStart + 1) }); }
//************************************************************************* /// <summary> /// Given the range to compute the diff on, this method finds the point: /// (midOriginal, midModified) /// that exists in the middle of the LCS of the two sequences and /// is the point at which the LCS problem may be broken down recursively. /// This method will try to keep the LCS trace in memory. If the LCS recursion /// point is calculated and the full trace is available in memory, then this method /// will return the change list. /// </summary> /// <param name="originalStart">The start bound of the original sequence range</param> /// <param name="originalEnd">The end bound of the original sequence range</param> /// <param name="modifiedStart">The start bound of the modified sequence range</param> /// <param name="modifiedEnd">The end bound of the modified sequence range</param> /// <param name="midOriginal">The middle point of the original sequence range</param> /// <param name="midModified">The middle point of the modified sequence range</param> /// <returns>The diff changes, if available, otherwise null</returns> //************************************************************************* private IDiffChange[] ComputeRecursionPoint(int originalStart, int originalEnd, int modifiedStart, int modifiedEnd, out int midOriginal, out int midModified, out bool quitEarly) { int originalIndex, modifiedIndex; int diagonalForwardStart = 0, diagonalForwardEnd = 0; int diagonalReverseStart = 0, diagonalReverseEnd = 0; int numDifferences; // To traverse the edit graph and produce the proper LCS, our actual // start position is just outside the given boundary originalStart--; modifiedStart--; // We set these up to make the compiler happy, but they will // be replaced before we return with the actual recursion point midOriginal = 0; midModified = 0; // Clear out the history m_forwardHistory.Clear(); m_reverseHistory.Clear(); // Each cell in the two arrays corresponds to a diagonal in the edit graph. // The integer value in the cell represents the originalIndex of the furthest // reaching point found so far that ends in that diagonal. // The modifiedIndex can be computed mathematically from the originalIndex and the diagonal number. int maxDifferences = (originalEnd - originalStart) + (modifiedEnd - modifiedStart); int numDiagonals = maxDifferences + 1; int[] forwardPoints = new int[numDiagonals]; int[] reversePoints = new int[numDiagonals]; // diagonalForwardBase: Index into forwardPoints of the diagonal which passes through (originalStart, modifiedStart) // diagonalReverseBase: Index into reversePoints of the diagonal which passes through (originalEnd, modifiedEnd) int diagonalForwardBase = (modifiedEnd - modifiedStart); int diagonalReverseBase = (originalEnd - originalStart); // diagonalForwardOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the // diagonal number (relative to diagonalForwardBase) // diagonalReverseOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the // diagonal number (relative to diagonalReverseBase) int diagonalForwardOffset = (originalStart - modifiedStart); int diagonalReverseOffset = (originalEnd - modifiedEnd); // delta: The difference between the end diagonal and the start diagonal. This is used to relate diagonal numbers // relative to the start diagonal with diagonal numbers relative to the end diagonal. // The Even/Oddn-ness of this delta is important for determining when we should check for overlap int delta = diagonalReverseBase - diagonalForwardBase; bool deltaIsEven = (delta % 2 == 0); // Here we set up the start and end points as the furthest points found so far // in both the forward and reverse directions, respectively forwardPoints[diagonalForwardBase] = originalStart; reversePoints[diagonalReverseBase] = originalEnd; // Remember if we quit early, and thus need to do a best-effort result instead of a real result. quitEarly = false; // A couple of points: // --With this method, we iterate on the number of differences between the two sequences. // The more differences there actually are, the longer this will take. // --Also, as the number of differences increases, we have to search on diagonals further // away from the reference diagonal (which is diagonalForwardBase for forward, diagonalReverseBase for reverse). // --We extend on even diagonals (relative to the reference diagonal) only when numDifferences // is even and odd diagonals only when numDifferences is odd. for (numDifferences = 1; numDifferences <= (maxDifferences / 2) + 1; numDifferences++) { int furthestOriginalIndex = 0; int furthestModifiedIndex = 0; // Run the algorithm in the forward direction diagonalForwardStart = ClipDiagonalBound(diagonalForwardBase - numDifferences, numDifferences, diagonalForwardBase, numDiagonals); diagonalForwardEnd = ClipDiagonalBound(diagonalForwardBase + numDifferences, numDifferences, diagonalForwardBase, numDiagonals); for (int diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2) { // STEP 1: We extend the furthest reaching point in the present diagonal // by looking at the diagonals above and below and picking the one whose point // is further away from the start point (originalStart, modifiedStart) if (diagonal == diagonalForwardStart || (diagonal < diagonalForwardEnd && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) { originalIndex = forwardPoints[diagonal + 1]; } else { originalIndex = forwardPoints[diagonal - 1] + 1; } modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset; // Save the current originalIndex so we can test for false overlap in step 3 int tempOriginalIndex = originalIndex; // STEP 2: We can continue to extend the furthest reaching point in the present diagonal // so long as the elements are equal. while (originalIndex < originalEnd && modifiedIndex < modifiedEnd && ElementsAreEqual(originalIndex + 1, modifiedIndex + 1)) { originalIndex++; modifiedIndex++; } forwardPoints[diagonal] = originalIndex; if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex) { furthestOriginalIndex = originalIndex; furthestModifiedIndex = modifiedIndex; } // STEP 3: If delta is odd (overlap first happens on forward when delta is odd) // and diagonal is in the range of reverse diagonals computed for numDifferences-1 // (the previous iteration; we havent computed reverse diagonals for numDifferences yet) // then check for overlap. if (!deltaIsEven && Math.Abs(diagonal - diagonalReverseBase) <= (numDifferences - 1)) { if (originalIndex >= reversePoints[diagonal]) { midOriginal = originalIndex; midModified = modifiedIndex; if (tempOriginalIndex <= reversePoints[diagonal] && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) { // BINGO! We overlapped, and we have the full trace in memory! goto WALKTRACE; } else { // Either false overlap, or we didn't have enough memory for the full trace // Just return the recursion point return(null); } } } } // Check to see if we should be quitting early, before moving on to the next iteration. int matchLengthOfLongest = ((furthestOriginalIndex - originalStart) + (furthestModifiedIndex - modifiedStart) - numDifferences) / 2; if (ContinueDifferencePredicate != null && !ContinueDifferencePredicate(furthestOriginalIndex, OriginalSequence, matchLengthOfLongest)) { // We can't finish, so skip ahead to generating a result from what we have. quitEarly = true; // Use the furthest distance we got in the forward direction. midOriginal = furthestOriginalIndex; midModified = furthestModifiedIndex; if (matchLengthOfLongest > 0 && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) { // Enough of the history is in memory to walk it backwards goto WALKTRACE; } else { // We didn't actually remember enough of the history. //Since we are quiting the diff early, we need to shift back the originalStart and modified start //back into the boundary limits since we decremented their value above beyond the boundary limit. originalStart++; modifiedStart++; return(new DiffChange[] { new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, modifiedEnd - modifiedStart + 1) }); } } // Run the algorithm in the reverse direction diagonalReverseStart = ClipDiagonalBound(diagonalReverseBase - numDifferences, numDifferences, diagonalReverseBase, numDiagonals); diagonalReverseEnd = ClipDiagonalBound(diagonalReverseBase + numDifferences, numDifferences, diagonalReverseBase, numDiagonals); for (int diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2) { // STEP 1: We extend the furthest reaching point in the present diagonal // by looking at the diagonals above and below and picking the one whose point // is further away from the start point (originalEnd, modifiedEnd) if (diagonal == diagonalReverseStart || (diagonal < diagonalReverseEnd && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) { originalIndex = reversePoints[diagonal + 1] - 1; } else { originalIndex = reversePoints[diagonal - 1]; } modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset; // Save the current originalIndex so we can test for false overlap int tempOriginalIndex = originalIndex; // STEP 2: We can continue to extend the furthest reaching point in the present diagonal // as long as the elements are equal. while (originalIndex > originalStart && modifiedIndex > modifiedStart && ElementsAreEqual(originalIndex, modifiedIndex)) { originalIndex--; modifiedIndex--; } reversePoints[diagonal] = originalIndex; // STEP 4: If delta is even (overlap first happens on reverse when delta is even) // and diagonal is in the range of forward diagonals computed for numDifferences // then check for overlap. if (deltaIsEven && Math.Abs(diagonal - diagonalForwardBase) <= numDifferences) { if (originalIndex <= forwardPoints[diagonal]) { midOriginal = originalIndex; midModified = modifiedIndex; if (tempOriginalIndex >= forwardPoints[diagonal] && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) { // BINGO! We overlapped, and we have the full trace in memory! goto WALKTRACE; } else { // Either false overlap, or we didn't have enough memory for the full trace // Just return the recursion point return(null); } } } } // Save current vectors to history before the next iteration if (numDifferences <= MaxDifferencesHistory) { // We are allocating space for one extra int, which we fill with // the index of the diagonal base index int[] temp = new int[diagonalForwardEnd - diagonalForwardStart + 2]; temp[0] = diagonalForwardBase - diagonalForwardStart + 1; Array.Copy(forwardPoints, diagonalForwardStart, temp, 1, diagonalForwardEnd - diagonalForwardStart + 1); m_forwardHistory.Add(temp); temp = new int[diagonalReverseEnd - diagonalReverseStart + 2]; temp[0] = diagonalReverseBase - diagonalReverseStart + 1; Array.Copy(reversePoints, diagonalReverseStart, temp, 1, diagonalReverseEnd - diagonalReverseStart + 1); m_reverseHistory.Add(temp); } } // If we got here, then we have the full trace in history. We just have to convert it to a change list // NOTE: This part is a bit messy WALKTRACE : IDiffChange[] forwardChanges, reverseChanges; // First, walk backward through the forward diagonals history using (DiffChangeHelper changeHelper = new DiffChangeHelper()) { int diagonalMin = diagonalForwardStart; int diagonalMax = diagonalForwardEnd; int diagonalRelative = (midOriginal - midModified) - diagonalForwardOffset; int lastOriginalIndex = Int32.MinValue; int historyIndex = m_forwardHistory.Count - 1; do { // Get the diagonal index from the relative diagonal number int diagonal = diagonalRelative + diagonalForwardBase; // Figure out where we came from if (diagonal == diagonalMin || (diagonal < diagonalMax && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) { // Vertical line (the element is an insert) originalIndex = forwardPoints[diagonal + 1]; modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset; if (originalIndex < lastOriginalIndex) { changeHelper.MarkNextChange(); } lastOriginalIndex = originalIndex; changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex); diagonalRelative = (diagonal + 1) - diagonalForwardBase; //Setup for the next iteration } else { // Horizontal line (the element is a deletion) originalIndex = forwardPoints[diagonal - 1] + 1; modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset; if (originalIndex < lastOriginalIndex) { changeHelper.MarkNextChange(); } lastOriginalIndex = originalIndex - 1; changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1); diagonalRelative = (diagonal - 1) - diagonalForwardBase; //Setup for the next iteration } if (historyIndex >= 0) { forwardPoints = m_forwardHistory[historyIndex]; diagonalForwardBase = forwardPoints[0]; //We stored this in the first spot diagonalMin = 1; diagonalMax = forwardPoints.Length - 1; } } while (--historyIndex >= -1); // Ironically, we get the forward changes as the reverse of the // order we added them since we technically added them backwards forwardChanges = changeHelper.ReverseChanges; } if (quitEarly) { // Since we did quit early, assume everything after the midOriginal/midModified point is a diff int originalStartPoint = midOriginal + 1; int modifiedStartPoint = midModified + 1; if (forwardChanges != null && forwardChanges.Length > 0) { IDiffChange lastForwardChange = forwardChanges[forwardChanges.Length - 1]; originalStartPoint = Math.Max(originalStartPoint, lastForwardChange.OriginalEnd); modifiedStartPoint = Math.Max(modifiedStartPoint, lastForwardChange.ModifiedEnd); } reverseChanges = new DiffChange[] { new DiffChange(originalStartPoint, originalEnd - originalStartPoint + 1, modifiedStartPoint, modifiedEnd - modifiedStartPoint + 1) }; } else { // Now walk backward through the reverse diagonals history using (DiffChangeHelper changeHelper = new DiffChangeHelper()) { int diagonalMin = diagonalReverseStart; int diagonalMax = diagonalReverseEnd; int diagonalRelative = (midOriginal - midModified) - diagonalReverseOffset; int lastOriginalIndex = Int32.MaxValue; int historyIndex = (deltaIsEven) ? m_reverseHistory.Count - 1 : m_reverseHistory.Count - 2; do { // Get the diagonal index from the relative diagonal number int diagonal = diagonalRelative + diagonalReverseBase; // Figure out where we came from if (diagonal == diagonalMin || (diagonal < diagonalMax && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) { // Horizontal line (the element is a deletion)) originalIndex = reversePoints[diagonal + 1] - 1; modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset; if (originalIndex > lastOriginalIndex) { changeHelper.MarkNextChange(); } lastOriginalIndex = originalIndex + 1; changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1); diagonalRelative = (diagonal + 1) - diagonalReverseBase; //Setup for the next iteration } else { // Vertical line (the element is an insertion) originalIndex = reversePoints[diagonal - 1]; modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset; if (originalIndex > lastOriginalIndex) { changeHelper.MarkNextChange(); } lastOriginalIndex = originalIndex; changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1); diagonalRelative = (diagonal - 1) - diagonalReverseBase; //Setup for the next iteration } if (historyIndex >= 0) { reversePoints = m_reverseHistory[historyIndex]; diagonalReverseBase = reversePoints[0]; //We stored this in the first spot diagonalMin = 1; diagonalMax = reversePoints.Length - 1; } } while (--historyIndex >= -1); // There are cases where the reverse history will find diffs that // are correct, but not intuitive, so we need shift them. reverseChanges = changeHelper.Changes; } } return(ConcatenateChanges(forwardChanges, reverseChanges)); }
/// <summary> /// Visual compare <see cref="IDiffChange"/> with the Visual Studio Diff Tool. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void CompareChangeRegion(IDiffChange diffChange) { const string SourceFileTag = "Server"; const string TargetFileTag = "Local"; const bool IsSourceReadOnly = true; const bool IsTargetReadOnly = true; const bool DeleteSourceOnExit = true; const bool DeleteTargetOnExit = true; const string FileLabelTemplateSingleLine = "{0};{1}[line {2}]"; const string FileLabelTemplateBetweenLines = "{0};{1}[between lines {3}-{2}]"; const string FileLabelTemplateLinesRange = "{0};{1}[lines {2}-{3}]"; string sourceFileLabelTemplate; if (diffChange.OriginalStart == diffChange.OriginalEnd) sourceFileLabelTemplate = FileLabelTemplateSingleLine; else if (diffChange.ChangeType == DiffChangeType.Insert) sourceFileLabelTemplate = FileLabelTemplateBetweenLines; else sourceFileLabelTemplate = FileLabelTemplateLinesRange; string sourceFileLabel = string.Format( sourceFileLabelTemplate, _marginCore.VersionControlItem.ServerItem, "T;" /* Latest version token */, diffChange.OriginalStart + 1, diffChange.OriginalEnd + 1); string targetFileLabelTemplate; if (diffChange.ModifiedStart == diffChange.ModifiedEnd) targetFileLabelTemplate = FileLabelTemplateSingleLine; else if (diffChange.ChangeType == DiffChangeType.Delete) targetFileLabelTemplate = FileLabelTemplateBetweenLines; else targetFileLabelTemplate = FileLabelTemplateLinesRange; string targetFileLabel = string.Format( targetFileLabelTemplate, _marginCore.TextDocument.FilePath, string.Empty /* No additional parameter */, diffChange.ModifiedStart + 1, diffChange.ModifiedEnd + 1); const bool RemoveLastLineTerminator = true; string sourceText = _marginCore.GetOriginalText(diffChange, RemoveLastLineTerminator); string sourceFilePath = System.IO.Path.GetTempFileName(); if (!string.IsNullOrEmpty(sourceText)) File.WriteAllText(sourceFilePath, sourceText); string targetText = _marginCore.GetModifiedText(diffChange, RemoveLastLineTerminator); string targetFilePath = System.IO.Path.GetTempFileName(); if (!string.IsNullOrEmpty(targetText)) File.WriteAllText(targetFilePath, targetText); Difference.VisualDiffFiles( sourceFilePath, targetFilePath, SourceFileTag, TargetFileTag, sourceFileLabel, targetFileLabel, IsSourceReadOnly, IsTargetReadOnly, DeleteSourceOnExit, DeleteTargetOnExit); }
/// <summary> /// Adds a line of text from an <see cref="ITextSnapshot"/> to <see cref="DiffLinesCollection"/>. /// </summary> /// <param name="collection">Collection of differences between the current document and the version in TFS.</param> /// <param name="textSnapshot">The text snapshot.</param> /// <param name="lineNumber">The line number.</param> /// <param name="diffChangeInfo">Represents information about a specific difference between two sequences.</param> private static void AddLineToDiffLinesCollection(DiffLinesCollection collection, ITextSnapshot textSnapshot, int lineNumber, IDiffChange diffChangeInfo) { ITextSnapshotLine line; try { line = textSnapshot.GetLineFromLineNumber(lineNumber); } catch (ArgumentOutOfRangeException ex) { string msg = string.Format("Line number {0} is out of range [0..{1}].", lineNumber, textSnapshot.LineCount); throw new ArgumentOutOfRangeException(msg, ex); } collection.Add(line, diffChangeInfo); }
/// <summary> /// Get a local text from the modified sequence of the <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> /// <param name="removeLastLineTerminator">Remove line terminator for the last line of the returned text, if line terminator is present.</param> /// <returns>Modified text from the <see cref="IDiffChange"/>.</returns> public string GetModifiedText(IDiffChange diffChange, bool removeLastLineTerminator) { if (diffChange.ChangeType == DiffChangeType.Delete) return string.Empty; return GetModifiedText(diffChange.ModifiedStart, diffChange.ModifiedEnd, removeLastLineTerminator); }
/// <summary> /// Visual compare <see cref="IDiffChange"/> with the Visual Studio Diff Tool. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> private void CompareChangeRegion(IDiffChange diffChange) { const string SourceFileTag = "Server"; const string TargetFileTag = "Local"; const bool IsSourceReadOnly = true; const bool IsTargetReadOnly = true; const bool DeleteSourceOnExit = true; const bool DeleteTargetOnExit = true; const string FileLabelTemplateSingleLine = "{0};{1}[line {2}]"; const string FileLabelTemplateBetweenLines = "{0};{1}[between lines {3}-{2}]"; const string FileLabelTemplateLinesRange = "{0};{1}[lines {2}-{3}]"; string sourceFileLabelTemplate; if (diffChange.OriginalStart == diffChange.OriginalEnd) { sourceFileLabelTemplate = FileLabelTemplateSingleLine; } else if (diffChange.ChangeType == DiffChangeType.Insert) { sourceFileLabelTemplate = FileLabelTemplateBetweenLines; } else { sourceFileLabelTemplate = FileLabelTemplateLinesRange; } string sourceFileLabel = string.Format( sourceFileLabelTemplate, _marginCore.VersionControlItem.ServerItem, "T;" /* Latest version token */, diffChange.OriginalStart + 1, diffChange.OriginalEnd + 1); string targetFileLabelTemplate; if (diffChange.ModifiedStart == diffChange.ModifiedEnd) { targetFileLabelTemplate = FileLabelTemplateSingleLine; } else if (diffChange.ChangeType == DiffChangeType.Delete) { targetFileLabelTemplate = FileLabelTemplateBetweenLines; } else { targetFileLabelTemplate = FileLabelTemplateLinesRange; } string targetFileLabel = string.Format( targetFileLabelTemplate, _marginCore.TextDocument.FilePath, string.Empty /* No additional parameter */, diffChange.ModifiedStart + 1, diffChange.ModifiedEnd + 1); const bool RemoveLastLineTerminator = true; string sourceText = _marginCore.GetOriginalText(diffChange, RemoveLastLineTerminator); string sourceFilePath = System.IO.Path.GetTempFileName(); if (!string.IsNullOrEmpty(sourceText)) { File.WriteAllText(sourceFilePath, sourceText); } string targetText = _marginCore.GetModifiedText(diffChange, RemoveLastLineTerminator); string targetFilePath = System.IO.Path.GetTempFileName(); if (!string.IsNullOrEmpty(targetText)) { File.WriteAllText(targetFilePath, targetText); } Difference.VisualDiffFiles( sourceFilePath, targetFilePath, SourceFileTag, TargetFileTag, sourceFileLabel, targetFileLabel, IsSourceReadOnly, IsTargetReadOnly, DeleteSourceOnExit, DeleteTargetOnExit); }
/// <summary> /// Get a committed text from the original sequence of the <see cref="IDiffChange"/>. /// </summary> /// <param name="diffChange">Information about a specific difference between two sequences.</param> /// <param name="removeLastLineTerminator">Remove line terminator for the last line of the returned text, if line terminator is present.</param> /// <returns>Original text from the <see cref="IDiffChange"/>.</returns> public string GetOriginalText(IDiffChange diffChange, bool removeLastLineTerminator) { if (diffChange.ChangeType == DiffChangeType.Insert) return string.Empty; return GetOriginalText(diffChange.OriginalStart, diffChange.OriginalEnd, removeLastLineTerminator); }
/// <summary> /// Draw margins for each diff line. /// </summary> /// <param name="diffLines">Differences between the current document and the version in TFS.</param> private void DrawMargins(DiffLinesCollection diffLines) { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => DrawMargins(diffLines)); return; } Children.Clear(); foreach (ITextViewLine viewLine in _textView.TextViewLines) { DiffChangeType diffType; ITextSnapshotLine line; try { if (ContainsChanges(viewLine, diffLines[DiffChangeType.Change], out line)) { diffType = DiffChangeType.Change; } else if (ContainsChanges(viewLine, diffLines[DiffChangeType.Insert], out line)) { diffType = DiffChangeType.Insert; ITextSnapshotLine tmp; if (ContainsChanges(viewLine, diffLines[DiffChangeType.Delete], out tmp)) { diffType = DiffChangeType.Change; line = tmp; } } else if (ContainsChanges(viewLine, diffLines[DiffChangeType.Delete], out line)) { diffType = DiffChangeType.Delete; } else { continue; } } catch (ArgumentException) { // The supplied line is on an incorrect snapshot (old version). return; } IDiffChange diffChangeInfo = diffLines[line]; var rect = new Rectangle { Height = viewLine.Height, Width = MarginElementWidth, Cursor = Cursors.Hand, ContextMenu = _contextMenu, Tag = new { DiffChangeInfo = diffChangeInfo, Line = line } }; SetLeft(rect, MarginElementLeft); SetTop(rect, viewLine.Top - _textView.ViewportTop); switch (diffType) { case DiffChangeType.Insert: rect.Fill = _marginCore.MarginSettings.AddedLineMarginBrush; break; case DiffChangeType.Change: rect.Fill = _marginCore.MarginSettings.ModifiedLineMarginBrush; break; case DiffChangeType.Delete: rect.Fill = _marginCore.MarginSettings.RemovedLineMarginBrush; break; default: throw new ArgumentOutOfRangeException(); } if (diffType != DiffChangeType.Insert) { rect.MouseEnter += OnMarginElementMouseEnter; } rect.MouseLeftButtonDown += OnMarginElementMouseLeftButtonDown; rect.MouseLeftButtonUp += OnMarginElementMouseLeftButtonUp; Children.Add(rect); } }