feature<Diff>: highlights differences for modified lines (both added and removed)

This commit is contained in:
leo 2021-07-28 15:02:06 +08:00
parent 776defe6c3
commit 92de6f2b79
7 changed files with 142 additions and 29 deletions

View file

@ -6,7 +6,7 @@ cd src
rmdir /s /q bin rmdir /s /q bin
rmdir /s /q obj rmdir /s /q obj
dotnet publish SourceGit_48.csproj --nologo -c Release -r win-x86 -o ..\publish\net48 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 cd ..\publish
ren SourceGit.exe SourceGit_48.exe ren SourceGit.exe SourceGit_48.exe
rmdir /s /q net48 rmdir /s /q net48

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands {
@ -8,6 +9,8 @@ namespace SourceGit.Commands {
public class Diff : Command { public class Diff : Command {
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
private Models.TextChanges changes = new Models.TextChanges(); private Models.TextChanges changes = new Models.TextChanges();
private List<Models.TextChanges.Line> deleted = new List<Models.TextChanges.Line>();
private List<Models.TextChanges.Line> added = new List<Models.TextChanges.Line>();
private int oldLine = 0; private int oldLine = 0;
private int newLine = 0; private int newLine = 0;
@ -18,6 +21,7 @@ namespace SourceGit.Commands {
public Models.TextChanges Result() { public Models.TextChanges Result() {
Exec(); Exec();
ProcessChanges();
if (changes.IsBinary) changes.Lines.Clear(); if (changes.IsBinary) changes.Lines.Clear();
return changes; return changes;
} }
@ -37,6 +41,7 @@ namespace SourceGit.Commands {
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", "")); changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", ""));
} else { } else {
if (line.Length == 0) { if (line.Length == 0) {
ProcessChanges();
changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}")); changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Normal, "", $"{oldLine}", $"{newLine}"));
oldLine++; oldLine++;
newLine++; newLine++;
@ -45,12 +50,13 @@ namespace SourceGit.Commands {
var ch = line[0]; var ch = line[0];
if (ch == '-') { 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++; oldLine++;
} else if (ch == '+') { } 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++; newLine++;
} else if (ch != '\\') { } else if (ch != '\\') {
ProcessChanges();
var match = REG_INDICATOR.Match(line); var match = REG_INDICATOR.Match(line);
if (match.Success) { if (match.Success) {
oldLine = int.Parse(match.Groups[1].Value); 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();
}
}
} }
} }

View file

@ -14,11 +14,18 @@ namespace SourceGit.Models {
Deleted, 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 class Line {
public LineMode Mode = LineMode.Normal; public LineMode Mode = LineMode.Normal;
public string Content = ""; public string Content = "";
public string OldLine = ""; public string OldLine = "";
public string NewLine = ""; public string NewLine = "";
public List<HighlightRange> Highlights = new List<HighlightRange>();
public Line(LineMode mode, string content, string oldLine, string newLine) { public Line(LineMode mode, string content, string oldLine, string newLine) {
Mode = mode; Mode = mode;

View file

@ -17,4 +17,7 @@
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<SatelliteResourceLanguages>none</SatelliteResourceLanguages> <SatelliteResourceLanguages>none</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.0" />
</ItemGroup>
</Project> </Project>

View file

@ -21,5 +21,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DiffPlex" Version="1.7.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -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 {
/// <summary>
/// 支持部分高亮的文本组件
/// </summary>
public class HighlightableTextBlock : TextBlock {
public class Data {
public string Text { get; set; } = "";
public List<Models.TextChanges.HighlightRange> Highlights { get; set; } = new List<Models.TextChanges.HighlightRange>();
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)));
}
}
}
}

View file

@ -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_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_DELETED = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
private static readonly Brush BG_NORMAL = Brushes.Transparent; 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 class Option {
public string[] RevisionRange = new string[] { }; public string[] RevisionRange = new string[] { };
@ -29,13 +32,13 @@ namespace SourceGit.Views.Widgets {
} }
public class Block { public class Block {
public string Content { get; set; }
public Models.TextChanges.LineMode Mode { get; set; } public Models.TextChanges.LineMode Mode { get; set; }
public Brush BG { get; set; } public Brush BG { get; set; }
public Brush FG { get; set; } public Brush FG { get; set; }
public FontStyle Style { get; set; } public FontStyle Style { get; set; }
public string OldLine { get; set; } public string OldLine { get; set; }
public string NewLine { get; set; } public string NewLine { get; set; }
public Controls.HighlightableTextBlock.Data Data { get; set; } = new Controls.HighlightableTextBlock.Data();
public bool IsContent { public bool IsContent {
get { get {
@ -216,14 +219,15 @@ namespace SourceGit.Views.Widgets {
foreach (var line in cachedTextChanges) { foreach (var line in cachedTextChanges) {
var block = new Block(); var block = new Block();
block.Content = line.Content;
block.Mode = line.Mode; block.Mode = line.Mode;
block.BG = GetLineBackground(line); block.BG = GetLineBackground(line);
block.FG = block.IsContent ? fgCommon : fgIndicator; block.FG = block.IsContent ? fgCommon : fgIndicator;
block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic; block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic;
block.OldLine = line.OldLine; block.OldLine = line.OldLine;
block.NewLine = line.NewLine; 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.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine; if (line.NewLine.Length > 0) lastNewLine = line.NewLine;
@ -278,13 +282,15 @@ namespace SourceGit.Views.Widgets {
foreach (var line in cachedTextChanges) { foreach (var line in cachedTextChanges) {
var block = new Block(); var block = new Block();
block.Content = line.Content;
block.Mode = line.Mode; block.Mode = line.Mode;
block.BG = GetLineBackground(line); block.BG = GetLineBackground(line);
block.FG = block.IsContent ? fgCommon : fgIndicator; block.FG = block.IsContent ? fgCommon : fgIndicator;
block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic; block.Style = block.IsContent ? FontStyles.Normal : FontStyles.Italic;
block.OldLine = line.OldLine; block.OldLine = line.OldLine;
block.NewLine = line.NewLine; 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.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine; 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<Block> old, List<Block> cur) { private void FillEmptyLines(List<Block> old, List<Block> cur) {
if (old.Count < cur.Count) { if (old.Count < cur.Count) {
int diff = cur.Count - old.Count; int diff = cur.Count - old.Count;
for (int i = 0; i < diff; i++) { for (int i = 0; i < diff; i++) {
var empty = new Block(); var empty = new Block();
empty.Content = "";
empty.Mode = Models.TextChanges.LineMode.None; empty.Mode = Models.TextChanges.LineMode.None;
empty.BG = BG_EMPTY; empty.BG = BG_EMPTY;
empty.FG = Brushes.Transparent; empty.FG = Brushes.Transparent;
@ -389,7 +405,6 @@ namespace SourceGit.Views.Widgets {
for (int i = 0; i < diff; i++) { for (int i = 0; i < diff; i++) {
var empty = new Block(); var empty = new Block();
empty.Content = "";
empty.Mode = Models.TextChanges.LineMode.None; empty.Mode = Models.TextChanges.LineMode.None;
empty.BG = BG_EMPTY; empty.BG = BG_EMPTY;
empty.FG = Brushes.Transparent; empty.FG = Brushes.Transparent;
@ -432,7 +447,7 @@ namespace SourceGit.Views.Widgets {
if (block == null) continue; if (block == null) continue;
if (!block.IsContent) continue; if (!block.IsContent) continue;
builder.Append(block.Content); builder.Append(block.Data.Text);
builder.AppendLine(); builder.AppendLine();
} }
@ -447,27 +462,21 @@ namespace SourceGit.Views.Widgets {
grid.Columns.Add(colLineNumber); grid.Columns.Add(colLineNumber);
} }
var borderContent = new FrameworkElementFactory(typeof(Border)); var line = new FrameworkElementFactory(typeof(Controls.HighlightableTextBlock));
borderContent.SetBinding(Border.BackgroundProperty, new Binding("BG")); line.SetBinding(Controls.HighlightableTextBlock.ContentProperty, new Binding("Data"));
line.SetBinding(TextBlock.BackgroundProperty, new Binding("BG"));
var textContent = new FrameworkElementFactory(typeof(TextBlock)); line.SetBinding(TextBlock.ForegroundProperty, new Binding("FG"));
textContent.SetBinding(TextBlock.TextProperty, new Binding("Content")); line.SetBinding(TextBlock.FontStyleProperty, new Binding("Style"));
textContent.SetBinding(TextBlock.ForegroundProperty, new Binding("FG")); line.SetValue(TextBlock.FontSizeProperty, new FontSizeConverter().ConvertFrom("9pt"));
textContent.SetBinding(TextBlock.FontStyleProperty, new Binding("Style")); line.SetValue(TextBlock.MarginProperty, new Thickness(0));
textContent.SetValue(TextBlock.BackgroundProperty, Brushes.Transparent); line.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0));
textContent.SetValue(TextBlock.FontSizeProperty, new FontSizeConverter().ConvertFrom("9pt")); line.SetValue(TextBlock.LineHeightProperty, 16.0);
textContent.SetValue(TextBlock.MarginProperty, new Thickness(0)); line.SetValue(TextOptions.TextFormattingModeProperty, TextFormattingMode.Display);
textContent.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0)); line.SetValue(TextOptions.TextRenderingModeProperty, TextRenderingMode.ClearType);
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 colContent = new DataGridTemplateColumn(); var colContent = new DataGridTemplateColumn();
colContent.CellTemplate = new DataTemplate(); colContent.CellTemplate = new DataTemplate();
colContent.CellTemplate.VisualTree = visualTree; colContent.CellTemplate.VisualTree = line;
colContent.Width = DataGridLength.SizeToCells; colContent.Width = DataGridLength.SizeToCells;
grid.Columns.Add(colContent); grid.Columns.Add(colContent);
@ -567,7 +576,7 @@ namespace SourceGit.Views.Widgets {
if (block == null) continue; if (block == null) continue;
if (!block.IsContent) continue; if (!block.IsContent) continue;
builder.Append(block.Content); builder.Append(block.Data.Text);
builder.AppendLine(); builder.AppendLine();
} }