From cbdebee4c2a4ccf9c470ef3dc51e5a562b74bf98 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 24 Nov 2020 17:14:44 +0800 Subject: [PATCH] feature<*>: use DataGrid instead of RichTextBox to improve performance --- src/Git/Blame.cs | 15 +- src/Git/Diff.cs | 186 ++--------- src/Git/Repository.cs | 29 +- src/UI/Blame.xaml | 116 +++---- src/UI/Blame.xaml.cs | 246 +++++++-------- src/UI/CommitViewer.xaml | 2 + src/UI/CommitViewer.xaml.cs | 5 +- src/UI/DiffViewer.xaml | 235 +++++++------- src/UI/DiffViewer.xaml.cs | 608 ++++++++++++++++-------------------- 9 files changed, 594 insertions(+), 848 deletions(-) diff --git a/src/Git/Blame.cs b/src/Git/Blame.cs index bf597274..7985aaa2 100644 --- a/src/Git/Blame.cs +++ b/src/Git/Blame.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace SourceGit.Git { @@ -8,9 +8,9 @@ namespace SourceGit.Git { public class Blame { /// - /// Block content. + /// Line content. /// - public class Block { + public class Line { public string CommitSHA { get; set; } public string Author { get; set; } public string Time { get; set; } @@ -18,18 +18,13 @@ namespace SourceGit.Git { } /// - /// Blocks + /// Lines /// - public List Blocks { get; set; } = new List(); + public List Lines { get; set; } = new List(); /// /// Is binary file? /// public bool IsBinary { get; set; } = false; - - /// - /// Line count. - /// - public int LineCount { get; set; } = 0; } } diff --git a/src/Git/Diff.cs b/src/Git/Diff.cs index ba754fe6..273e58f8 100644 --- a/src/Git/Diff.cs +++ b/src/Git/Diff.cs @@ -15,100 +15,36 @@ namespace SourceGit.Git { /// Line mode. /// public enum LineMode { + None, Normal, Indicator, - Empty, Added, Deleted, } /// - /// Side + /// Line change. /// - public enum Side { - Left, - Right, - Both, - } - - /// - /// Block - /// - public class Block { - public Side Side = Side.Both; + public class LineChange { public LineMode Mode = LineMode.Normal; - public int LeftStart = 0; - public int RightStart = 0; - public int Count = 0; - public StringBuilder Builder = new StringBuilder(); + public string Content = ""; + public string OldLine = ""; + public string NewLine = ""; - public bool IsLeftDelete => Side == Side.Left && Mode == LineMode.Deleted; - public bool IsRightAdded => Side == Side.Right && Mode == LineMode.Added; - public bool IsBothSideNormal => Side == Side.Both && Mode == LineMode.Normal; - public bool CanShowNumber => Mode != LineMode.Indicator && Mode != LineMode.Empty; - - public void Append(string data) { - if (Count > 0) Builder.AppendLine(); - Builder.Append(data); - Count++; + public LineChange(LineMode mode, string content, string oldLine = "", string newLine = "") { + Mode = mode; + Content = content; + OldLine = oldLine; + NewLine = newLine; } } /// - /// Text file change. + /// Text change. /// public class TextChange { - public bool IsValid = false; + public List Lines = new List(); public bool IsBinary = false; - public List Blocks = new List(); - public int LeftLineCount = 0; - public int RightLineCount = 0; - - public void SetBinary() { - IsValid = true; - IsBinary = true; - } - - public void Add(Block b) { - if (b.Count == 0) return; - - switch (b.Side) { - case Side.Left: - LeftLineCount += b.Count; - break; - case Side.Right: - RightLineCount += b.Count; - break; - default: - LeftLineCount += b.Count; - RightLineCount += b.Count; - break; - } - - Blocks.Add(b); - } - - public void Fit() { - if (LeftLineCount > RightLineCount) { - var b = new Block(); - b.Side = Side.Right; - b.Mode = LineMode.Empty; - - var delta = LeftLineCount - RightLineCount; - for (int i = 0; i < delta; i++) b.Append(""); - - Add(b); - } else if (LeftLineCount < RightLineCount) { - var b = new Block(); - b.Side = Side.Left; - b.Mode = LineMode.Empty; - - var delta = RightLineCount - LeftLineCount; - for (int i = 0; i < delta; i++) b.Append(""); - - Add(b); - } - } } /// @@ -136,103 +72,49 @@ namespace SourceGit.Git { /// public static TextChange GetTextChange(Repository repo, string args) { var rs = new TextChange(); - var current = new Block(); - var left = 0; - var right = 0; + var started = false; + var oldLine = 0; + var newLine = 0; repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => { if (rs.IsBinary) return; - if (!rs.IsValid) { + if (!started) { var match = REG_INDICATOR.Match(line); if (!match.Success) { - if (line.StartsWith("Binary ")) rs.SetBinary(); + if (line.StartsWith("Binary ")) rs.IsBinary = true; return; } - rs.IsValid = true; - left = int.Parse(match.Groups[1].Value); - right = int.Parse(match.Groups[2].Value); - current.Mode = LineMode.Indicator; - current.Append(line); + started = true; + oldLine = int.Parse(match.Groups[1].Value); + newLine = int.Parse(match.Groups[2].Value); + rs.Lines.Add(new LineChange(LineMode.Indicator, line)); } else { if (line[0] == '-') { - if (current.IsLeftDelete) { - current.Append(line.Substring(1)); - } else { - rs.Add(current); - - current = new Block(); - current.Side = Side.Left; - current.Mode = LineMode.Deleted; - current.LeftStart = left; - current.Append(line.Substring(1)); - } - - left++; + rs.Lines.Add(new LineChange(LineMode.Deleted, line.Substring(1), $"{oldLine}", "")); + oldLine++; } else if (line[0] == '+') { - if (current.IsRightAdded) { - current.Append(line.Substring(1)); - } else { - rs.Add(current); - - current = new Block(); - current.Side = Side.Right; - current.Mode = LineMode.Added; - current.RightStart = right; - current.Append(line.Substring(1)); - } - - right++; + rs.Lines.Add(new LineChange(LineMode.Added, line.Substring(1), "", $"{newLine}")); + newLine++; } else if (line[0] == '\\') { - var tmp = new Block(); - tmp.Side = current.Side; - tmp.Mode = LineMode.Indicator; - tmp.Append(line.Substring(1)); - - rs.Add(current); - rs.Add(tmp); - rs.Fit(); - - current = new Block(); - current.LeftStart = left; - current.RightStart = right; + rs.Lines.Add(new LineChange(LineMode.Indicator, line.Substring(1))); } else { var match = REG_INDICATOR.Match(line); if (match.Success) { - rs.Add(current); - rs.Fit(); - - left = int.Parse(match.Groups[1].Value); - right = int.Parse(match.Groups[2].Value); - - current = new Block(); - current.Mode = LineMode.Indicator; - current.Append(line); + oldLine = int.Parse(match.Groups[1].Value); + newLine = int.Parse(match.Groups[2].Value); + rs.Lines.Add(new LineChange(LineMode.Indicator, line)); } else { - if (current.IsBothSideNormal) { - current.Append(line.Substring(1)); - } else { - rs.Add(current); - rs.Fit(); - - current = new Block(); - current.LeftStart = left; - current.RightStart = right; - current.Append(line.Substring(1)); - } - - left++; - right++; + rs.Lines.Add(new LineChange(LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}")); + oldLine++; + newLine++; } } } }); - rs.Add(current); - rs.Fit(); - - if (rs.IsBinary) rs.Blocks.Clear(); + if (rs.IsBinary) rs.Lines.Clear(); return rs; } diff --git a/src/Git/Repository.cs b/src/Git/Repository.cs index f0bbfe03..62f52291 100644 --- a/src/Git/Repository.cs +++ b/src/Git/Repository.cs @@ -945,7 +945,6 @@ namespace SourceGit.Git { public Blame BlameFile(string file, string revision) { var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); var blame = new Blame(); - var current = null as Blame.Block; var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => { if (blame.IsBinary) return; @@ -953,7 +952,7 @@ namespace SourceGit.Git { if (line.IndexOf('\0') >= 0) { blame.IsBinary = true; - blame.Blocks.Clear(); + blame.Lines.Clear(); return; } @@ -961,25 +960,19 @@ namespace SourceGit.Git { if (!match.Success) return; var commit = match.Groups[1].Value; + var author = match.Groups[2].Value; + var timestamp = int.Parse(match.Groups[3].Value); var data = match.Groups[4].Value; - if (current != null && current.CommitSHA == commit) { - current.Content = current.Content + "\n" + data; - } else { - var timestamp = int.Parse(match.Groups[3].Value); - var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); - current = new Blame.Block() { - CommitSHA = commit, - Author = match.Groups[2].Value, - Time = when, - Content = data, - }; + var blameLine = new Blame.Line() { + CommitSHA = commit, + Author = author, + Time = when, + Content = data, + }; - if (current.Author == null) current.Author = ""; - blame.Blocks.Add(current); - } - - blame.LineCount++; + blame.Lines.Add(blameLine); }); if (errs != null) App.RaiseError(errs); diff --git a/src/UI/Blame.xaml b/src/UI/Blame.xaml index f356c551..443b314c 100644 --- a/src/UI/Blame.xaml +++ b/src/UI/Blame.xaml @@ -107,80 +107,60 @@ - + - - - - - - - + + - - - - - - + + + + + - - - - - - - + + + - - - - - - + + - - - - - - - - - - + + + + + + + + + + + + - + @@ -197,7 +177,7 @@ - + diff --git a/src/UI/Blame.xaml.cs b/src/UI/Blame.xaml.cs index 624d67e3..77736953 100644 --- a/src/UI/Blame.xaml.cs +++ b/src/UI/Blame.xaml.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -15,6 +14,9 @@ namespace SourceGit.UI { /// Viewer to show git-blame /// public partial class Blame : Window { + private Git.Repository repo = null; + private string lastSHA = null; + private int lastBG = 1; /// /// Background color for blocks. @@ -24,17 +26,24 @@ namespace SourceGit.UI { new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) }; + /// + /// Record + /// + public class Record { + public Git.Blame.Line Line { get; set; } + public Brush BG { get; set; } + public int LineNumber { get; set; } + } + /// /// Constructor /// - /// + /// /// /// - public Blame(Git.Repository repo, string file, string revision) { + public Blame(Git.Repository open, string file, string revision) { InitializeComponent(); - double minWidth = content.ActualWidth; - // Move to center. var parent = App.Current.MainWindow; Left = parent.Left + (parent.Width - Width) * 0.5; @@ -48,97 +57,57 @@ namespace SourceGit.UI { // Layout content blameFile.Content = $"{file}@{revision.Substring(0, 8)}"; + repo = open; + Task.Run(() => { - var blame = repo.BlameFile(file, revision); + var result = repo.BlameFile(file, revision); + var records = new List(); + + if (result.IsBinary) { + var error = new Record(); + error.Line = new Git.Blame.Line() { Content = "BINARY FILE BLAME NOT SUPPORTED!!!", CommitSHA = null }; + error.BG = Brushes.Red; + error.LineNumber = 0; + records.Add(error); + } else { + int count = 1; + foreach (var line in result.Lines) { + var r = new Record(); + r.Line = line; + r.BG = GetBG(line); + r.LineNumber = count; + + records.Add(r); + count++; + } + } Dispatcher.Invoke(() => { - content.Document.Blocks.Clear(); - - if (blame.IsBinary) { - lineNumber.ItemsSource = null; - - Paragraph p = new Paragraph(new Run("BINARY FILE BLAME NOT SUPPORTED!!!")); - p.Margin = new Thickness(0); - p.Padding = new Thickness(0); - p.LineHeight = 1; - p.Background = Brushes.Transparent; - p.Foreground = FindResource("Brush.FG") as SolidColorBrush; - p.FontStyle = FontStyles.Normal; - - content.Document.Blocks.Add(p); - } else { - List numbers = new List(); - for (int i = 0; i < blame.LineCount; i++) numbers.Add(i.ToString()); - lineNumber.ItemsSource = numbers; - - var fg = FindResource("Brush.FG") as SolidColorBrush; - var tf = new Typeface(content.FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); - var ns = new NumberSubstitution(); - var mp = new Thickness(0); - - for (int i = 0; i < blame.Blocks.Count; i++) { - var frag = blame.Blocks[i]; - var idx = i; - - Paragraph p = new Paragraph(new Run(frag.Content)); - p.DataContext = frag; - p.Margin = mp; - p.Padding = mp; - p.LineHeight = 1; - p.Background = BG[i % 2]; - p.Foreground = fg; - p.FontStyle = FontStyles.Normal; - p.ContextMenuOpening += (sender, ev) => { - if (!content.Selection.IsEmpty) return; - - Hyperlink link = new Hyperlink(new Run(frag.CommitSHA)); - link.ToolTip = "CLICK TO GO"; - link.Click += (o, e) => { - repo.OnNavigateCommit?.Invoke(frag.CommitSHA); - e.Handled = true; - }; - - foreach (var block in content.Document.Blocks) { - var paragraph = block as Paragraph; - if ((paragraph.DataContext as Git.Blame.Block).CommitSHA == frag.CommitSHA) { - paragraph.Background = Brushes.Green; - } else { - paragraph.Background = BG[i % 2]; - } - } - - commitID.Content = link; - authorName.Content = frag.Author; - authorTime.Content = frag.Time; - popup.IsOpen = true; - ev.Handled = true; - }; - - var formatter = new FormattedText( - frag.Content, - CultureInfo.CurrentUICulture, - FlowDirection.LeftToRight, - tf, - content.FontSize, - Brushes.Black, - ns, - TextFormattingMode.Ideal); - if (minWidth < formatter.Width) { - content.Document.PageWidth = formatter.Width + 16; - minWidth = formatter.Width; - } - - content.Document.Blocks.Add(p); - } - } - - // Hide loading. loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null); loading.Visibility = Visibility.Collapsed; + + blame.ItemsSource = records; + blame.UpdateLayout(); + + ContentSizeChanged(null, null); }); }); } + /// + /// Get background brush. + /// + /// + /// + private Brush GetBG(Git.Blame.Line line) { + if (lastSHA != line.CommitSHA) { + lastSHA = line.CommitSHA; + lastBG = 1 - lastBG; + } + + return BG[lastBG]; + } + /// /// Click logo /// @@ -177,68 +146,83 @@ namespace SourceGit.UI { Close(); } - /// - /// Sync scroll - /// - /// - /// - private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) { - if (e.VerticalChange != 0) { - var margin = new Thickness(4, -e.VerticalOffset, 4, 0); - lineNumber.Margin = margin; - } - } - - /// - /// Mouse wheel - /// - /// - /// - private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) { - if (e.Delta > 0) { - content.LineUp(); - } else { - content.LineDown(); - } - - e.Handled = true; - } - /// /// Content size changed. /// /// /// private void ContentSizeChanged(object sender, SizeChangedEventArgs e) { - if (content.Document.PageWidth < content.ActualWidth) { - content.Document.PageWidth = content.ActualWidth; - } + var total = area.ActualWidth; + var offset = blame.NonFrozenColumnsViewportHorizontalOffset; + var minWidth = total - offset - 2; + + var scroller = GetVisualChild(blame); + if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; + + blame.Columns[1].MinWidth = minWidth; + blame.Columns[1].Width = DataGridLength.SizeToCells; + blame.UpdateLayout(); } /// - /// Auto scroll when selection changed. + /// Context menu opening. /// /// /// - private void ContentSelectionChanged(object sender, RoutedEventArgs e) { - var doc = sender as RichTextBox; - if (doc == null || doc.IsFocused == false) return; + private void OnBlameContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var item = sender as DataGridRow; + if (item == null) return; - if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) { - var p = Mouse.GetPosition(doc); + var record = item.DataContext as Record; + if (record == null || record.Line.CommitSHA == null) return; - if (p.X <= 8) { - doc.LineLeft(); - } else if (p.X >= doc.ActualWidth - 8) { - doc.LineRight(); + Hyperlink link = new Hyperlink(new Run(record.Line.CommitSHA)); + link.ToolTip = "CLICK TO GO"; + link.Click += (o, e) => { + repo.OnNavigateCommit?.Invoke(record.Line.CommitSHA); + e.Handled = true; + }; + + commitID.Content = link; + authorName.Content = record.Line.Author; + authorTime.Content = record.Line.Time; + popup.IsOpen = true; + ev.Handled = true; + } + + /// + /// Prevent auto scroll. + /// + /// + /// + private void OnBlameRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { + e.Handled = true; + } + + /// + /// Find child element of type. + /// + /// + /// + /// + private T GetVisualChild(DependencyObject parent) where T : Visual { + T child = null; + + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) { + Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); + child = v as T; + + if (child == null) { + child = GetVisualChild(v); } - if (p.Y <= 8) { - doc.LineUp(); - } else if (p.Y >= doc.ActualHeight - 8) { - doc.LineDown(); + if (child != null) { + break; } } + + return child; } } } diff --git a/src/UI/CommitViewer.xaml b/src/UI/CommitViewer.xaml index 02f683b2..45d5aeac 100644 --- a/src/UI/CommitViewer.xaml +++ b/src/UI/CommitViewer.xaml @@ -299,6 +299,7 @@ @@ -388,6 +389,7 @@ diff --git a/src/UI/CommitViewer.xaml.cs b/src/UI/CommitViewer.xaml.cs index 1810a5c3..a2199b00 100644 --- a/src/UI/CommitViewer.xaml.cs +++ b/src/UI/CommitViewer.xaml.cs @@ -551,7 +551,10 @@ namespace SourceGit.UI { menu.IsOpen = true; e.Handled = true; } - #endregion + private void TreeRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { + e.Handled = true; + } + #endregion } } diff --git a/src/UI/DiffViewer.xaml b/src/UI/DiffViewer.xaml index 7ad6b7c0..41dbe78d 100644 --- a/src/UI/DiffViewer.xaml +++ b/src/UI/DiffViewer.xaml @@ -7,12 +7,29 @@ mc:Ignorable="d" FontFamily="Consolas"> - + + + + + + + @@ -52,140 +69,106 @@ - + - - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + - - - - - + x:Name="textChangeNewSide" + GridLinesVisibility="Vertical" + VerticalGridLinesBrush="{StaticResource Brush.Border2}" + FrozenColumnCount="1" + ScrollViewer.ScrollChanged="OnTwoSidesScroll" + ContextMenuOpening="OnTextChangeContextMenuOpening"> - - - - - - - + + + - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public partial class DiffViewer : UserControl { - private double minWidth = 0; - private Git.Diff.TextChange textChangeData = null; + private List lineChanges = null; + private Brush bgEmpty = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); + private Brush bgAdded = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); + private Brush bgDeleted = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)); + private Brush bgNormal = Brushes.Transparent; /// /// Diff options. @@ -27,6 +29,19 @@ namespace SourceGit.UI { public string ExtraArgs = ""; } + /// + /// Change block. + /// + public class ChangeBlock { + public string Content { get; set; } + public Git.Diff.LineMode Mode { get; set; } + public Brush BG { get; set; } + public Brush FG { get; set; } + public FontStyle Style { get; set; } + public string OldLine { get; set; } + public string NewLine { get; set; } + } + /// /// Constructor /// @@ -50,11 +65,12 @@ namespace SourceGit.UI { public void Diff(Git.Repository repo, Option opts) { SetTitle(opts.Path, opts.OrgPath); - textChangeData = null; + lineChanges = null; loading.Visibility = Visibility.Visible; mask.Visibility = Visibility.Collapsed; - textChange.Visibility = Visibility.Collapsed; + textChangeOneSide.Visibility = Visibility.Collapsed; + textChangeTwoSides.Visibility = Visibility.Collapsed; sizeChange.Visibility = Visibility.Collapsed; noChange.Visibility = Visibility.Collapsed; @@ -82,8 +98,8 @@ namespace SourceGit.UI { var rs = Git.Diff.GetTextChange(repo, args); if (rs.IsBinary) { SetBinaryChange(Git.Diff.GetSizeChange(repo, opts.RevisionRange, opts.Path, opts.OrgPath)); - } else if (rs.Blocks.Count > 0) { - textChangeData = rs; + } else if (rs.Lines.Count > 0) { + lineChanges = rs.Lines; SetTextChange(); } else { SetSame(); @@ -112,42 +128,100 @@ namespace SourceGit.UI { /// /// private void SetTextChange() { - if (textChangeData == null) return; + if (lineChanges == null) return; - Dispatcher.Invoke(() => { - loading.Visibility = Visibility.Collapsed; - textChange.Visibility = Visibility.Visible; - textChangeOptions.Visibility = Visibility.Visible; + var fgCommon = FindResource("Brush.FG") as Brush; + var fgIndicator = FindResource("Brush.FG2") as Brush; - if (App.Preference.UIUseOneSideDiff) { - twoSideLeft.Width = new GridLength(0); - twoSideLeft.MinWidth = 0; - twoSideSplittter.Width = new GridLength(0); - } else { - twoSideLeft.Width = new GridLength(1, GridUnitType.Star); - twoSideLeft.MinWidth = 100; - twoSideSplittter.Width = new GridLength(2); + if (App.Preference.UIUseOneSideDiff) { + var blocks = new List(); + + foreach (var line in lineChanges) { + var block = new ChangeBlock(); + block.Content = line.Content; + block.Mode = line.Mode; + block.BG = GetLineBackground(line); + block.FG = line.Mode == Git.Diff.LineMode.Indicator ? fgIndicator : fgCommon; + block.Style = line.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal; + block.OldLine = line.OldLine; + block.NewLine = line.NewLine; + + blocks.Add(block); } - minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16; + Dispatcher.Invoke(() => { + loading.Visibility = Visibility.Collapsed; + textChangeOptions.Visibility = Visibility.Visible; + textChangeOneSide.Visibility = Visibility.Visible; + textChangeTwoSides.Visibility = Visibility.Collapsed; - leftLineNumber.ItemsSource = null; - rightLineNumber.ItemsSource = null; + ResetDataGrid(textChangeOneSide); + textChangeOneSide.ItemsSource = blocks; + OnSizeChanged(null, null); + }); + } else { + var oldSideBlocks = new List(); + var newSideBlocks = new List(); - leftText.Document.Blocks.Clear(); - rightText.Document.Blocks.Clear(); + foreach (var line in lineChanges) { + var block = new ChangeBlock(); + block.Content = line.Content; + block.Mode = line.Mode; + block.BG = GetLineBackground(line); + block.FG = line.Mode == Git.Diff.LineMode.Indicator ? fgIndicator : fgCommon; + block.Style = line.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal; + block.OldLine = line.OldLine; + block.NewLine = line.NewLine; - var lLineNumbers = new List(); - var rLineNumbers = new List(); + switch (line.Mode) { + case Git.Diff.LineMode.Added: + newSideBlocks.Add(block); - foreach (var b in textChangeData.Blocks) ShowBlock(b, lLineNumbers, rLineNumbers); + var oldEmpty = new ChangeBlock(); + oldEmpty.Content = ""; + oldEmpty.Mode = Git.Diff.LineMode.None; + oldEmpty.BG = bgEmpty; + oldEmpty.FG = fgCommon; + oldEmpty.Style = FontStyles.Normal; + oldEmpty.OldLine = block.OldLine; + oldEmpty.NewLine = block.NewLine; + oldSideBlocks.Add(oldEmpty); + break; + case Git.Diff.LineMode.Deleted: + oldSideBlocks.Add(block); - if (!App.Preference.UIUseOneSideDiff) leftText.Document.PageWidth = minWidth + 16; - rightText.Document.PageWidth = minWidth + 16; - leftLineNumber.ItemsSource = lLineNumbers; - rightLineNumber.ItemsSource = rLineNumbers; - leftText.ScrollToHome(); - }); + var newEmpty = new ChangeBlock(); + newEmpty.Content = ""; + newEmpty.Mode = Git.Diff.LineMode.None; + newEmpty.BG = bgEmpty; + newEmpty.FG = fgCommon; + newEmpty.Style = FontStyles.Normal; + newEmpty.OldLine = block.OldLine; + newEmpty.NewLine = block.NewLine; + newSideBlocks.Add(newEmpty); + break; + default: + oldSideBlocks.Add(block); + newSideBlocks.Add(block); + break; + } + } + + Dispatcher.Invoke(() => { + loading.Visibility = Visibility.Collapsed; + textChangeOptions.Visibility = Visibility.Visible; + textChangeOneSide.Visibility = Visibility.Collapsed; + textChangeTwoSides.Visibility = Visibility.Visible; + + ResetDataGrid(textChangeOldSide); + ResetDataGrid(textChangeNewSide); + + textChangeOldSide.ItemsSource = oldSideBlocks; + textChangeNewSide.ItemsSource = newSideBlocks; + + OnSizeChanged(null, null); + }); + } } /// @@ -195,277 +269,145 @@ namespace SourceGit.UI { } /// - /// Make paragraph for two-sides diff + /// Get background color of line. /// /// - /// - /// - private void ShowBlock(Git.Diff.Block b, List leftNumber, List rightNumber) { - bool useOneSide = App.Preference.UIUseOneSideDiff; - if (useOneSide && b.Mode == Git.Diff.LineMode.Empty) return; - - var content = b.Builder.ToString(); - - // Make paragraph element - Paragraph p = new Paragraph(new Run(content)); - p.Margin = new Thickness(0); - p.Padding = new Thickness(0); - p.LineHeight = 1; - p.Background = GetBlockBackground(b); - p.Foreground = b.Mode == Git.Diff.LineMode.Indicator ? Brushes.Gray : FindResource("Brush.FG") as SolidColorBrush; - p.FontStyle = b.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal; - p.DataContext = b; - p.ContextMenuOpening += OnParagraphContextMenuOpening; - - // Calculate with - var formatter = new FormattedText( - content, - CultureInfo.CurrentUICulture, - FlowDirection.LeftToRight, - new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch), - leftText.FontSize, - Brushes.Black, - new NumberSubstitution(), - TextFormattingMode.Ideal); - if (minWidth < formatter.Width) minWidth = formatter.Width; - - // Line numbers - switch (b.Side) { - case Git.Diff.Side.Left: - for (int i = 0; i < b.Count; i++) { - if (b.CanShowNumber) leftNumber.Add($"{i + b.LeftStart}"); - else leftNumber.Add(""); - - if (useOneSide) rightNumber.Add(""); - } - break; - case Git.Diff.Side.Right: - for (int i = 0; i < b.Count; i++) { - if (b.CanShowNumber) rightNumber.Add($"{i + b.RightStart}"); - else rightNumber.Add(""); - - if (useOneSide) leftNumber.Add(""); - } - break; + /// + private Brush GetLineBackground(Git.Diff.LineChange line) { + switch (line.Mode) { + case Git.Diff.LineMode.Added: + return bgAdded; + case Git.Diff.LineMode.Deleted: + return bgDeleted; default: - for (int i = 0; i < b.Count; i++) { - if (b.CanShowNumber) { - leftNumber.Add($"{i + b.LeftStart}"); - rightNumber.Add($"{i + b.RightStart}"); - } else { - leftNumber.Add(""); - rightNumber.Add(""); - } - } - break; - } - - // Add this paragraph to document. - if (App.Preference.UIUseOneSideDiff) { - rightText.Document.Blocks.Add(p); - } else { - switch (b.Side) { - case Git.Diff.Side.Left: - leftText.Document.Blocks.Add(p); - break; - case Git.Diff.Side.Right: - rightText.Document.Blocks.Add(p); - break; - default: - leftText.Document.Blocks.Add(p); - - var cp = new Paragraph(new Run(content)); - cp.Margin = new Thickness(0); - cp.Padding = new Thickness(); - cp.LineHeight = 1; - cp.Background = p.Background; - cp.Foreground = p.Foreground; - cp.FontStyle = p.FontStyle; - cp.DataContext = b; - cp.ContextMenuOpening += OnParagraphContextMenuOpening; - - rightText.Document.Blocks.Add(cp); - break; - } + return bgNormal; } } /// - /// Get background color of block. + /// Find child element of type. /// - /// + /// + /// /// - private Brush GetBlockBackground(Git.Diff.Block b) { - Border border = new Border(); - border.BorderThickness = new Thickness(0); - border.BorderBrush = Brushes.LightBlue; - border.Height = b.Count * 16 - 1; - border.Width = minWidth - 1; + private T GetVisualChild(DependencyObject parent) where T : Visual { + T child = null; - switch (b.Mode) { - case Git.Diff.LineMode.Empty: - border.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); - break; - case Git.Diff.LineMode.Added: - border.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); - break; - case Git.Diff.LineMode.Deleted: - border.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)); - break; - default: - border.Background = Brushes.Transparent; - break; + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) { + Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); + child = v as T; + + if (child == null) { + child = GetVisualChild(v); + } + + if (child != null) { + break; + } } - VisualBrush highlight = new VisualBrush(); - highlight.TileMode = TileMode.None; - highlight.Stretch = Stretch.Fill; - highlight.Visual = border; - return highlight; + return child; + } + + private void ResetDataGrid(DataGrid dg) { + dg.ItemsSource = null; + dg.Items.Clear(); + + foreach (var col in dg.Columns) { + col.MinWidth = 0; + col.Width = 0; + } } #endregion #region EVENTS - /// - /// Context menu for text-change paragraph - /// - /// - /// - private void OnParagraphContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var paragraph = sender as Paragraph; - - var doc = (paragraph.Parent as FlowDocument); - if (doc != null) { - var textBox = doc.Parent as RichTextBox; - if (textBox != null && !textBox.Selection.IsEmpty) { - var copyItem = new MenuItem(); - copyItem.Header = "Copy"; - copyItem.Click += (o, e) => { - Clipboard.SetText(textBox.Selection.Text); - e.Handled = true; - }; - - var copyMenu = new ContextMenu(); - copyMenu.Items.Add(copyItem); - copyMenu.IsOpen = true; - ev.Handled = true; - return; - } - } - - var block = paragraph.DataContext as Git.Diff.Block; - if (block.Mode == Git.Diff.LineMode.Empty || block.Mode == Git.Diff.LineMode.Indicator) { - ev.Handled = true; - return; - } - - var highlight = paragraph.Background as VisualBrush; - if (highlight != null) { - (highlight.Visual as Border).BorderThickness = new Thickness(.5); - } - - paragraph.ContextMenu = new ContextMenu(); - paragraph.ContextMenu.Closed += (o, e) => { - if (paragraph.ContextMenu == (o as ContextMenu)) { - if (highlight != null) { - (highlight.Visual as Border).BorderThickness = new Thickness(0); - } - paragraph.ContextMenu = null; - } - }; - - var copy = new MenuItem(); - copy.Header = "Copy"; - copy.Click += (o, e) => { - Clipboard.SetText(block.Builder.ToString()); - e.Handled = true; - }; - paragraph.ContextMenu.Items.Add(copy); - - paragraph.ContextMenu.IsOpen = true; - ev.Handled = true; - } /// - /// Fix document size. + /// Auto fit text change diff size. /// /// /// private void OnSizeChanged(object sender, SizeChangedEventArgs e) { - var text = sender as RichTextBox; - if (text.Document.PageWidth < text.ActualWidth) { - text.Document.PageWidth = text.ActualWidth; + var total = area.ActualWidth; + + if (App.Preference.UIUseOneSideDiff) { + textChangeOneSide.Columns[0].Width = DataGridLength.Auto; + textChangeOneSide.Columns[1].Width = DataGridLength.Auto; + textChangeOneSide.Columns[2].MinWidth = 1; + textChangeOneSide.Columns[2].Width = 1; + textChangeOneSide.UpdateLayout(); + + var offset = textChangeOneSide.NonFrozenColumnsViewportHorizontalOffset; + var minWidth = total - offset; + + var scroller = GetVisualChild(textChangeOneSide); + if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; + + textChangeOneSide.Columns[2].MinWidth = minWidth; + textChangeOneSide.Columns[2].Width = DataGridLength.Auto; + textChangeOneSide.UpdateLayout(); + } else { + textChangeOldSide.Columns[0].Width = DataGridLength.Auto; + textChangeOldSide.Columns[1].MinWidth = 1; + textChangeOldSide.Columns[1].Width = 1; + textChangeOldSide.UpdateLayout(); + + textChangeNewSide.Columns[0].Width = DataGridLength.Auto; + textChangeNewSide.Columns[1].MinWidth = 1; + textChangeNewSide.Columns[1].Width = 1; + textChangeNewSide.UpdateLayout(); + + var oldOffset = textChangeOldSide.NonFrozenColumnsViewportHorizontalOffset; + var newOffset = textChangeNewSide.NonFrozenColumnsViewportHorizontalOffset; + var minWidth = total - Math.Min(oldOffset, newOffset); + + var scroller = GetVisualChild(textChangeNewSide); + if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; + + textChangeOldSide.Columns[1].MinWidth = minWidth; + textChangeOldSide.Columns[1].Width = DataGridLength.Auto; + textChangeOldSide.UpdateLayout(); + + textChangeNewSide.Columns[1].MinWidth = minWidth; + textChangeNewSide.Columns[1].Width = DataGridLength.Auto; + textChangeNewSide.UpdateLayout(); } } /// - /// Scroll using mouse wheel. + /// Prevent default auto-scrolling when click row in DataGrid. /// /// /// - private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) { - var text = sender as RichTextBox; - if (text == null) return; - - if (e.Delta > 0) { - text.LineUp(); - } else { - text.LineDown(); - } - + private void OnLineRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { e.Handled = true; } /// - /// Sync scroll both sides. + /// Sync scroll on two sides diff. /// /// /// - private void OnViewerScroll(object sender, ScrollChangedEventArgs e) { + private void OnTwoSidesScroll(object sender, ScrollChangedEventArgs e) { + var oldSideScroller = GetVisualChild(textChangeOldSide); + var newSideScroller = GetVisualChild(textChangeNewSide); + if (e.VerticalChange != 0) { - if (leftText.VerticalOffset != e.VerticalOffset) { - leftText.ScrollToVerticalOffset(e.VerticalOffset); + if (oldSideScroller.VerticalOffset != e.VerticalOffset) { + oldSideScroller.ScrollToVerticalOffset(e.VerticalOffset); } - if (rightText.VerticalOffset != e.VerticalOffset) { - rightText.ScrollToVerticalOffset(e.VerticalOffset); + if (newSideScroller.VerticalOffset != e.VerticalOffset) { + newSideScroller.ScrollToVerticalOffset(e.VerticalOffset); } - - leftLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0); - rightLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0); } else { - if (leftText.HorizontalOffset != e.HorizontalOffset) { - leftText.ScrollToHorizontalOffset(e.HorizontalOffset); + if (oldSideScroller.HorizontalOffset != e.HorizontalOffset) { + oldSideScroller.ScrollToHorizontalOffset(e.HorizontalOffset); } - if (rightText.HorizontalOffset != e.HorizontalOffset) { - rightText.ScrollToHorizontalOffset(e.HorizontalOffset); - } - } - } - - /// - /// Auto scroll when selection changed. - /// - /// - /// - private void OnViewerSelectionChanged(object sender, RoutedEventArgs e) { - var doc = sender as RichTextBox; - if (doc == null || doc.IsFocused == false) return; - - if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) { - var p = Mouse.GetPosition(doc); - - if (p.X <= 8) { - doc.LineLeft(); - } else if (p.X >= doc.ActualWidth - 8) { - doc.LineRight(); - } - - if (p.Y <= 8) { - doc.LineUp(); - } else if (p.Y >= doc.ActualHeight - 8) { - doc.LineDown(); + if (newSideScroller.HorizontalOffset != e.HorizontalOffset) { + newSideScroller.ScrollToHorizontalOffset(e.HorizontalOffset); } } } @@ -476,46 +418,23 @@ namespace SourceGit.UI { /// /// private void Go2Next(object sender, RoutedEventArgs e) { - double minTop = 0; + var grid = textChangeOneSide; + if (!App.Preference.UIUseOneSideDiff) grid = textChangeNewSide; - if (App.Preference.UIUseOneSideDiff) { - foreach (var p in rightText.Document.Blocks) { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top > 17 && (block.IsLeftDelete || block.IsRightAdded)) { - minTop = rect.Top; + var scroller = GetVisualChild(grid); + var firstVisible = (int)scroller.VerticalOffset; + var firstModeEnded = false; + var first = grid.Items[firstVisible] as ChangeBlock; + for (int i = firstVisible + 1; i < grid.Items.Count; i++) { + var next = grid.Items[i] as ChangeBlock; + if (next.Mode != Git.Diff.LineMode.Normal && next.Mode != Git.Diff.LineMode.Indicator) { + if (firstModeEnded || next.Mode != first.Mode) { + scroller.ScrollToVerticalOffset(i); break; } + } else { + firstModeEnded = true; } - } else { - Paragraph next = null; - - foreach (var p in leftText.Document.Blocks) { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top > 17 && block.IsLeftDelete) { - next = p as Paragraph; - minTop = rect.Top; - break; - } - } - - foreach (var p in rightText.Document.Blocks) { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top > 17 && block.IsRightAdded) { - if (next == null || minTop > rect.Top) { - next = p as Paragraph; - minTop = rect.Top; - } - - break; - } - } - } - - if (minTop > 0) { - rightText.ScrollToVerticalOffset(rightText.VerticalOffset + minTop - 16); } } @@ -525,51 +444,23 @@ namespace SourceGit.UI { /// /// private void Go2Prev(object sender, RoutedEventArgs e) { - double maxTop = double.MaxValue; + var grid = textChangeOneSide; + if (!App.Preference.UIUseOneSideDiff) grid = textChangeNewSide; - if (App.Preference.UIUseOneSideDiff) { - var p = rightText.Document.Blocks.LastBlock as Paragraph; - do { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top < 15 && (block.IsLeftDelete || block.IsRightAdded)) { - maxTop = rect.Top; + var scroller = GetVisualChild(grid); + var firstVisible = (int)scroller.VerticalOffset; + var firstModeEnded = false; + var first = grid.Items[firstVisible] as ChangeBlock; + for (int i = firstVisible - 1; i >= 0; i--) { + var next = grid.Items[i] as ChangeBlock; + if (next.Mode != Git.Diff.LineMode.Normal && next.Mode != Git.Diff.LineMode.Indicator) { + if (firstModeEnded || next.Mode != first.Mode) { + scroller.ScrollToVerticalOffset(i); break; } - - p = p.PreviousBlock as Paragraph; - } while (p != null); - } else { - Paragraph next = null; - - var p = leftText.Document.Blocks.LastBlock as Paragraph; - do { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top < 15 && block.IsLeftDelete) { - next = p; - maxTop = rect.Top; - break; - } - - p = p.PreviousBlock as Paragraph; - } while (p != null); - - p = rightText.Document.Blocks.LastBlock as Paragraph; - do { - var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); - var block = p.DataContext as Git.Diff.Block; - if (rect.Top < 15 && block.IsRightAdded) { - if (next == null || maxTop < rect.Top) maxTop = rect.Top; - break; - } - - p = p.PreviousBlock as Paragraph; - } while (p != null); - } - - if (maxTop != double.MaxValue) { - rightText.ScrollToVerticalOffset(rightText.VerticalOffset + maxTop - 16); + } else { + firstModeEnded = true; + } } } @@ -581,6 +472,39 @@ namespace SourceGit.UI { private void ChangeDiffMode(object sender, RoutedEventArgs e) { SetTextChange(); } + + /// + /// Text change context menu opening. + /// + /// + /// + private void OnTextChangeContextMenuOpening(object sender, ContextMenuEventArgs e) { + var grid = sender as DataGrid; + if (grid == null) return; + + var menu = new ContextMenu(); + var copy = new MenuItem(); + copy.Header = "Copy Selected Lines"; + copy.Click += (o, ev) => { + var items = grid.SelectedItems; + if (items.Count == 0) return; + + var builder = new StringBuilder(); + foreach (var item in items) { + var block = item as ChangeBlock; + if (block == null) continue; + if (block.Mode == Git.Diff.LineMode.None || block.Mode == Git.Diff.LineMode.Indicator) continue; + + builder.Append(block.Content); + builder.AppendLine(); + } + + Clipboard.SetText(builder.ToString()); + }; + menu.Items.Add(copy); + menu.IsOpen = true; + e.Handled = true; + } #endregion } }