From 92de6f2b79130e189a33f6ae7724e6019cb6f8f4 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 28 Jul 2021 15:02:06 +0800 Subject: [PATCH] feature: highlights differences for modified lines (both added and removed) --- build.bat | 2 +- src/Commands/Diff.cs | 34 ++++++++++- src/Models/TextChanges.cs | 7 +++ src/SourceGit.csproj | 3 + src/SourceGit_48.csproj | 1 + src/Views/Controls/HighlightableTextBlock.cs | 63 ++++++++++++++++++++ src/Views/Widgets/DiffViewer.xaml.cs | 61 +++++++++++-------- 7 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 src/Views/Controls/HighlightableTextBlock.cs diff --git a/build.bat b/build.bat index b055284c..3bb6314f 100644 --- a/build.bat +++ b/build.bat @@ -6,7 +6,7 @@ cd src rmdir /s /q bin rmdir /s /q obj dotnet publish SourceGit_48.csproj --nologo -c Release -r win-x86 -o ..\publish\net48 -ilrepack /ndebug /out:..\publish\SourceGit.exe ..\publish\net48\SourceGit.exe ..\publish\net48\Newtonsoft.Json.dll +ilrepack /ndebug /out:..\publish\SourceGit.exe ..\publish\net48\SourceGit.exe ..\publish\net48\Newtonsoft.Json.dll ..\publish\net48\DiffPlex.dll cd ..\publish ren SourceGit.exe SourceGit_48.exe rmdir /s /q net48 diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 8be2af51..79e5945b 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; namespace SourceGit.Commands { @@ -8,6 +9,8 @@ namespace SourceGit.Commands { public class Diff : Command { private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); private Models.TextChanges changes = new Models.TextChanges(); + private List deleted = new List(); + private List added = new List(); private int oldLine = 0; private int newLine = 0; @@ -18,6 +21,7 @@ namespace SourceGit.Commands { public Models.TextChanges Result() { Exec(); + ProcessChanges(); if (changes.IsBinary) changes.Lines.Clear(); return changes; } @@ -37,6 +41,7 @@ namespace SourceGit.Commands { changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", "")); } else { if (line.Length == 0) { + ProcessChanges(); changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}")); oldLine++; newLine++; @@ -45,12 +50,13 @@ namespace SourceGit.Commands { var ch = line[0]; if (ch == '-') { - changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", "")); + deleted.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", "")); oldLine++; } else if (ch == '+') { - changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}")); + added.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}")); newLine++; } else if (ch != '\\') { + ProcessChanges(); var match = REG_INDICATOR.Match(line); if (match.Success) { oldLine = int.Parse(match.Groups[1].Value); @@ -64,5 +70,29 @@ namespace SourceGit.Commands { } } } + + private void ProcessChanges() { + if (deleted.Count > 0) { + if (added.Count == deleted.Count) { + for (int i = added.Count - 1; i >= 0; i--) { + var left = deleted[i]; + var right = added[i]; + var result = DiffPlex.Differ.Instance.CreateCharacterDiffs(left.Content, right.Content, false, false); + foreach (var block in result.DiffBlocks) { + left.Highlights.Add(new Models.TextChanges.HighlightRange(block.DeleteStartA, block.DeleteCountA)); + right.Highlights.Add(new Models.TextChanges.HighlightRange(block.InsertStartB, block.InsertCountB)); + } + } + } + + changes.Lines.AddRange(deleted); + deleted.Clear(); + } + + if (added.Count > 0) { + changes.Lines.AddRange(added); + added.Clear(); + } + } } } diff --git a/src/Models/TextChanges.cs b/src/Models/TextChanges.cs index f2700893..5555fad5 100644 --- a/src/Models/TextChanges.cs +++ b/src/Models/TextChanges.cs @@ -14,11 +14,18 @@ namespace SourceGit.Models { Deleted, } + public class HighlightRange { + public int Start { get; set; } + public int Count { get; set; } + public HighlightRange(int p, int n) { Start = p; Count = n; } + } + public class Line { public LineMode Mode = LineMode.Normal; public string Content = ""; public string OldLine = ""; public string NewLine = ""; + public List Highlights = new List(); public Line(LineMode mode, string content, string oldLine, string newLine) { Mode = mode; diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 8ab54a0a..cb4db08c 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -17,4 +17,7 @@ true none + + + \ No newline at end of file diff --git a/src/SourceGit_48.csproj b/src/SourceGit_48.csproj index aa37905b..f99fd1ed 100644 --- a/src/SourceGit_48.csproj +++ b/src/SourceGit_48.csproj @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/src/Views/Controls/HighlightableTextBlock.cs b/src/Views/Controls/HighlightableTextBlock.cs new file mode 100644 index 00000000..0c3d5003 --- /dev/null +++ b/src/Views/Controls/HighlightableTextBlock.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace SourceGit.Views.Controls { + /// + /// 支持部分高亮的文本组件 + /// + public class HighlightableTextBlock : TextBlock { + + public class Data { + public string Text { get; set; } = ""; + public List Highlights { get; set; } = new List(); + public Brush HighlightBrush { get; set; } = Brushes.Transparent; + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register( + "Content", + typeof(Data), + typeof(HighlightableTextBlock), + new PropertyMetadata(null, OnContentChanged)); + + public Data Content { + get { return (Data)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + var txt = d as HighlightableTextBlock; + if (txt == null) return; + + txt.Inlines.Clear(); + txt.Text = null; + if (txt.Content == null) return; + + if (txt.Content.Highlights == null || txt.Content.Highlights.Count == 0) { + txt.Text = txt.Content.Text; + return; + } + + var started = 0; + foreach (var highlight in txt.Content.Highlights) { + if (started < highlight.Start) { + txt.Inlines.Add(new Run(txt.Content.Text.Substring(started, highlight.Start - started))); + } + + txt.Inlines.Add(new TextBlock() { + Background = txt.Content.HighlightBrush, + LineHeight = txt.LineHeight, + Text = txt.Content.Text.Substring(highlight.Start, highlight.Count), + }); + + started = highlight.Start + highlight.Count; + } + + if (started < txt.Content.Text.Length) { + txt.Inlines.Add(new Run(txt.Content.Text.Substring(started))); + } + } + } +} diff --git a/src/Views/Widgets/DiffViewer.xaml.cs b/src/Views/Widgets/DiffViewer.xaml.cs index 36dff31c..10c15433 100644 --- a/src/Views/Widgets/DiffViewer.xaml.cs +++ b/src/Views/Widgets/DiffViewer.xaml.cs @@ -19,6 +19,9 @@ namespace SourceGit.Views.Widgets { private static readonly Brush BG_ADDED = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); private static readonly Brush BG_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)); private static readonly Brush BG_NORMAL = Brushes.Transparent; + private static readonly Brush HL_ADDED = new SolidColorBrush(Color.FromArgb(100, 0, 255, 0)); + private static readonly Brush HL_DELETED = new SolidColorBrush(Color.FromArgb(100, 255, 0, 0)); + private static readonly Brush HL_NORMAL = Brushes.Transparent; public class Option { public string[] RevisionRange = new string[] { }; @@ -29,13 +32,13 @@ namespace SourceGit.Views.Widgets { } public class Block { - public string Content { get; set; } public Models.TextChanges.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; } + public Controls.HighlightableTextBlock.Data Data { get; set; } = new Controls.HighlightableTextBlock.Data(); public bool IsContent { get { @@ -216,14 +219,15 @@ namespace SourceGit.Views.Widgets { foreach (var line in cachedTextChanges) { var block = new Block(); - block.Content = line.Content; block.Mode = line.Mode; block.BG = GetLineBackground(line); block.FG = block.IsContent ? fgCommon : fgIndicator; block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic; block.OldLine = line.OldLine; block.NewLine = line.NewLine; - + block.Data.Text = line.Content; + block.Data.HighlightBrush = GetLineHighlight(line); + block.Data.Highlights.AddRange(line.Highlights); if (line.OldLine.Length > 0) lastOldLine = line.OldLine; if (line.NewLine.Length > 0) lastNewLine = line.NewLine; @@ -278,13 +282,15 @@ namespace SourceGit.Views.Widgets { foreach (var line in cachedTextChanges) { var block = new Block(); - block.Content = line.Content; block.Mode = line.Mode; block.BG = GetLineBackground(line); block.FG = block.IsContent ? fgCommon : fgIndicator; block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic; block.OldLine = line.OldLine; block.NewLine = line.NewLine; + block.Data.Text = line.Content; + block.Data.HighlightBrush = GetLineHighlight(line); + block.Data.Highlights.AddRange(line.Highlights); if (line.OldLine.Length > 0) lastOldLine = line.OldLine; if (line.NewLine.Length > 0) lastNewLine = line.NewLine; @@ -369,13 +375,23 @@ namespace SourceGit.Views.Widgets { } } + private Brush GetLineHighlight(Models.TextChanges.Line line) { + switch (line.Mode) { + case Models.TextChanges.LineMode.Added: + return HL_ADDED; + case Models.TextChanges.LineMode.Deleted: + return HL_DELETED; + default: + return HL_NORMAL; + } + } + private void FillEmptyLines(List old, List cur) { if (old.Count < cur.Count) { int diff = cur.Count - old.Count; for (int i = 0; i < diff; i++) { var empty = new Block(); - empty.Content = ""; empty.Mode = Models.TextChanges.LineMode.None; empty.BG = BG_EMPTY; empty.FG = Brushes.Transparent; @@ -389,7 +405,6 @@ namespace SourceGit.Views.Widgets { for (int i = 0; i < diff; i++) { var empty = new Block(); - empty.Content = ""; empty.Mode = Models.TextChanges.LineMode.None; empty.BG = BG_EMPTY; empty.FG = Brushes.Transparent; @@ -432,7 +447,7 @@ namespace SourceGit.Views.Widgets { if (block == null) continue; if (!block.IsContent) continue; - builder.Append(block.Content); + builder.Append(block.Data.Text); builder.AppendLine(); } @@ -447,27 +462,21 @@ namespace SourceGit.Views.Widgets { grid.Columns.Add(colLineNumber); } - var borderContent = new FrameworkElementFactory(typeof(Border)); - borderContent.SetBinding(Border.BackgroundProperty, new Binding("BG")); - - var textContent = new FrameworkElementFactory(typeof(TextBlock)); - textContent.SetBinding(TextBlock.TextProperty, new Binding("Content")); - textContent.SetBinding(TextBlock.ForegroundProperty, new Binding("FG")); - textContent.SetBinding(TextBlock.FontStyleProperty, new Binding("Style")); - textContent.SetValue(TextBlock.BackgroundProperty, Brushes.Transparent); - textContent.SetValue(TextBlock.FontSizeProperty, new FontSizeConverter().ConvertFrom("9pt")); - textContent.SetValue(TextBlock.MarginProperty, new Thickness(0)); - textContent.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0)); - textContent.SetValue(TextOptions.TextFormattingModeProperty, TextFormattingMode.Display); - textContent.SetValue(TextOptions.TextRenderingModeProperty, TextRenderingMode.ClearType); - - var visualTree = new FrameworkElementFactory(typeof(Grid)); - visualTree.AppendChild(borderContent); - visualTree.AppendChild(textContent); + var line = new FrameworkElementFactory(typeof(Controls.HighlightableTextBlock)); + line.SetBinding(Controls.HighlightableTextBlock.ContentProperty, new Binding("Data")); + line.SetBinding(TextBlock.BackgroundProperty, new Binding("BG")); + line.SetBinding(TextBlock.ForegroundProperty, new Binding("FG")); + line.SetBinding(TextBlock.FontStyleProperty, new Binding("Style")); + line.SetValue(TextBlock.FontSizeProperty, new FontSizeConverter().ConvertFrom("9pt")); + line.SetValue(TextBlock.MarginProperty, new Thickness(0)); + line.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0)); + line.SetValue(TextBlock.LineHeightProperty, 16.0); + line.SetValue(TextOptions.TextFormattingModeProperty, TextFormattingMode.Display); + line.SetValue(TextOptions.TextRenderingModeProperty, TextRenderingMode.ClearType); var colContent = new DataGridTemplateColumn(); colContent.CellTemplate = new DataTemplate(); - colContent.CellTemplate.VisualTree = visualTree; + colContent.CellTemplate.VisualTree = line; colContent.Width = DataGridLength.SizeToCells; grid.Columns.Add(colContent); @@ -567,7 +576,7 @@ namespace SourceGit.Views.Widgets { if (block == null) continue; if (!block.IsContent) continue; - builder.Append(block.Content); + builder.Append(block.Data.Text); builder.AppendLine(); }