sourcegit/src/Views/Widgets/DiffViewer.xaml.cs

639 lines
25 KiB
C#
Raw Normal View History

2021-04-29 05:05:55 -07:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 变更对比视图
/// </summary>
public partial class DiffViewer : UserControl {
private static readonly Brush BG_EMPTY = new SolidColorBrush(Color.FromArgb(40, 0, 0, 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_NORMAL = Brushes.Transparent;
public class Option {
public string[] RevisionRange = new string[] { };
public string Path = "";
public string OrgPath = null;
public string ExtraArgs = "";
}
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 bool IsContent {
get {
return Mode == Models.TextChanges.LineMode.Added
|| Mode == Models.TextChanges.LineMode.Deleted
|| Mode == Models.TextChanges.LineMode.Normal;
}
}
public bool IsDifference {
get {
return Mode == Models.TextChanges.LineMode.Added
|| Mode == Models.TextChanges.LineMode.Deleted
|| Mode == Models.TextChanges.LineMode.None;
}
}
}
private ulong seq = 0;
2021-04-29 05:05:55 -07:00
private string repo = null;
private Option opt = null;
private List<Models.TextChanges.Line> cachedTextChanges = null;
private List<DataGrid> editors = new List<DataGrid>();
private List<Rectangle> splitters = new List<Rectangle>();
public DiffViewer() {
InitializeComponent();
Reset();
}
public void Reload() {
if (repo == null || opt == null) {
Reset();
} else {
Diff(repo, opt);
}
}
2021-04-29 05:05:55 -07:00
public void Reset() {
seq++;
2021-04-29 05:05:55 -07:00
mask.Visibility = Visibility.Visible;
toolbar.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed;
ClearCache();
foreach (var e in editors) e.ItemsSource = null;
foreach (var s in splitters) s.Visibility = Visibility.Hidden;
2021-04-29 05:05:55 -07:00
}
public void Diff(string repo, Option opt) {
seq++;
2021-04-29 05:05:55 -07:00
mask.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed;
toolbar.Visibility = Visibility.Visible;
loading.Visibility = Visibility.Visible;
loading.IsAnimating = true;
SetTitle(opt.Path, opt.OrgPath);
ClearCache();
this.repo = repo;
this.opt = opt;
var dummy = seq;
2021-04-29 05:05:55 -07:00
Task.Run(() => {
var args = $"{opt.ExtraArgs} ";
if (opt.RevisionRange.Length > 0) args += $"{opt.RevisionRange[0]} ";
if (opt.RevisionRange.Length > 1) args += $"{opt.RevisionRange[1]} ";
args += "-- ";
if (!string.IsNullOrEmpty(opt.OrgPath)) args += $"\"{opt.OrgPath}\" ";
args += $"\"{opt.Path}\"";
var isLFSObject = new Commands.IsLFSFiltered(repo, opt.Path).Result();
if (isLFSObject) {
var lc = new Commands.QueryLFSObjectChange(repo, args).Result();
if (lc.IsValid) {
SetLFSChange(lc, dummy);
2021-04-29 05:05:55 -07:00
} else {
SetSame(dummy);
2021-04-29 05:05:55 -07:00
}
return;
}
var rs = new Commands.Diff(repo, args).Result();
if (rs.IsBinary) {
var fsc = new Commands.QueryFileSizeChange(repo, opt.RevisionRange, opt.Path, opt.OrgPath).Result();
SetSizeChange(fsc, dummy);
2021-04-29 05:05:55 -07:00
} else if (rs.Lines.Count > 0) {
cachedTextChanges = rs.Lines;
SetTextChange(dummy);
2021-04-29 05:05:55 -07:00
} else {
SetSame(dummy);
2021-04-29 05:05:55 -07:00
}
});
}
#region LAYOUT_DATA
private void SetTitle(string file, string orgFile) {
txtFileName.Text = file;
if (!string.IsNullOrEmpty(orgFile) && orgFile != "/dev/null") {
orgFileNamePanel.Visibility = Visibility.Visible;
txtOrgFileName.Text = orgFile;
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed;
}
}
private void SetTextChange(ulong dummy) {
2021-04-29 05:05:55 -07:00
if (cachedTextChanges == null) return;
if (Models.Preference.Instance.Window.UseCombinedDiff) {
MakeCombinedViewer(dummy);
2021-04-29 05:05:55 -07:00
} else {
MakeSideBySideViewer(dummy);
2021-04-29 05:05:55 -07:00
}
}
private void SetSizeChange(Models.FileSizeChange fsc, ulong dummy) {
2021-04-29 05:05:55 -07:00
Dispatcher.Invoke(() => {
if (dummy != seq) return;
2021-04-29 05:05:55 -07:00
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Visible;
txtSizeChangeTitle.Text = App.Text("Diff.Binary");
iconSizeChange.Data = FindResource("Icon.Binary") as Geometry;
txtOldSize.Text = App.Text("Bytes", fsc.OldSize);
txtNewSize.Text = App.Text("Bytes", fsc.NewSize);
});
}
private void SetLFSChange(Models.LFSChange lc, ulong dummy) {
2021-04-29 05:05:55 -07:00
Dispatcher.Invoke(() => {
if (dummy != seq) return;
2021-04-29 05:05:55 -07:00
var oldSize = lc.Old == null ? 0 : lc.Old.Size;
var newSize = lc.New == null ? 0 : lc.New.Size;
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Visible;
txtSizeChangeTitle.Text = App.Text("Diff.LFS");
iconSizeChange.Data = FindResource("Icon.LFS") as Geometry;
txtNewSize.Text = App.Text("Bytes", newSize);
txtOldSize.Text = App.Text("Bytes", oldSize);
});
}
private void SetSame(ulong dummy) {
2021-04-29 05:05:55 -07:00
Dispatcher.Invoke(() => {
if (dummy != seq) return;
2021-04-29 05:05:55 -07:00
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Visible;
});
}
private void MakeCombinedViewer(ulong dummy) {
2021-04-29 05:05:55 -07:00
var fgCommon = FindResource("Brush.FG1") as Brush;
var fgIndicator = FindResource("Brush.FG2") as Brush;
var lastOldLine = "";
var lastNewLine = "";
var blocks = new List<Block>();
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;
if (line.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine;
blocks.Add(block);
}
Dispatcher.Invoke(() => {
if (dummy != seq) return;
2021-04-29 05:05:55 -07:00
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Visible;
var createEditor = editors.Count == 0;
var lineNumberWidth = CalcLineNumberColWidth(lastOldLine, lastNewLine);
var minWidth = textDiff.ActualWidth - lineNumberWidth * 2;
if (textDiff.ActualHeight < cachedTextChanges.Count * 16) minWidth -= 8;
DataGrid editor;
if (createEditor) {
editor = CreateTextEditor(new string[] { "OldLine", "NewLine" });
editor.SetValue(Grid.ColumnProperty, 0);
editor.SetValue(Grid.ColumnSpanProperty, 2);
editors.Add(editor);
textDiff.Children.Add(editor);
AddSplitter(0, Math.Floor(lineNumberWidth));
AddSplitter(0, Math.Floor(lineNumberWidth) * 2);
} else {
editor = editors[0];
splitters[0].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
splitters[1].Margin = new Thickness(Math.Floor(lineNumberWidth) * 2, 0, 0, 0);
}
foreach (var s in splitters) s.Visibility = Visibility.Visible;
2021-04-29 05:05:55 -07:00
editor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
editor.Columns[1].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
editor.Columns[2].MinWidth = minWidth;
editor.SetBinding(DataGrid.ItemsSourceProperty, new Binding() { Source = blocks, IsAsync = true });
2021-04-29 05:05:55 -07:00
});
}
private void MakeSideBySideViewer(ulong dummy) {
2021-04-29 05:05:55 -07:00
var fgCommon = FindResource("Brush.FG1") as Brush;
var fgIndicator = FindResource("Brush.FG2") as Brush;
var lastOldLine = "";
var lastNewLine = "";
var oldSideBlocks = new List<Block>();
var newSideBlocks = new List<Block>();
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;
if (line.OldLine.Length > 0) lastOldLine = line.OldLine;
if (line.NewLine.Length > 0) lastNewLine = line.NewLine;
switch (line.Mode) {
case Models.TextChanges.LineMode.Added:
newSideBlocks.Add(block);
break;
case Models.TextChanges.LineMode.Deleted:
oldSideBlocks.Add(block);
break;
default:
FillEmptyLines(oldSideBlocks, newSideBlocks);
oldSideBlocks.Add(block);
newSideBlocks.Add(block);
break;
}
}
FillEmptyLines(oldSideBlocks, newSideBlocks);
Dispatcher.Invoke(() => {
if (dummy != seq) return;
2021-04-29 05:05:55 -07:00
loading.Visibility = Visibility.Collapsed;
mask.Visibility = Visibility.Collapsed;
toolbarOptions.Visibility = Visibility.Visible;
var createEditor = editors.Count == 0;
var lineNumberWidth = CalcLineNumberColWidth(lastOldLine, lastNewLine);
var minWidth = textDiff.ActualWidth / 2 - lineNumberWidth;
if (textDiff.ActualHeight < newSideBlocks.Count * 16) minWidth -= 8;
DataGrid oldEditor, newEditor;
if (createEditor) {
oldEditor = CreateTextEditor(new string[] { "OldLine" });
oldEditor.SetValue(Grid.ColumnProperty, 0);
oldEditor.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnTextDiffSyncScroll));
newEditor = CreateTextEditor(new string[] { "NewLine" });
newEditor.SetValue(Grid.ColumnProperty, 1);
newEditor.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnTextDiffSyncScroll));
editors.Add(oldEditor);
editors.Add(newEditor);
textDiff.Children.Add(oldEditor);
textDiff.Children.Add(newEditor);
AddSplitter(0, Math.Floor(lineNumberWidth));
AddSplitter(1, 0);
AddSplitter(1, Math.Floor(lineNumberWidth));
} else {
oldEditor = editors[0];
newEditor = editors[1];
splitters[0].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
splitters[2].Margin = new Thickness(Math.Floor(lineNumberWidth), 0, 0, 0);
}
foreach (var s in splitters) s.Visibility = Visibility.Visible;
2021-04-29 05:05:55 -07:00
oldEditor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
oldEditor.Columns[1].MinWidth = minWidth;
oldEditor.SetBinding(DataGrid.ItemsSourceProperty, new Binding() { Source = oldSideBlocks, IsAsync = true });
2021-04-29 05:05:55 -07:00
newEditor.Columns[0].Width = new DataGridLength(lineNumberWidth, DataGridLengthUnitType.Pixel);
newEditor.Columns[1].MinWidth = minWidth;
newEditor.SetBinding(DataGrid.ItemsSourceProperty, new Binding() { Source = newSideBlocks, IsAsync = true });
2021-04-29 05:05:55 -07:00
});
}
private Brush GetLineBackground(Models.TextChanges.Line line) {
switch (line.Mode) {
case Models.TextChanges.LineMode.Added:
return BG_ADDED;
case Models.TextChanges.LineMode.Deleted:
return BG_DELETED;
default:
return BG_NORMAL;
}
}
private void FillEmptyLines(List<Block> old, List<Block> 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;
empty.Style = FontStyles.Normal;
empty.OldLine = "";
empty.NewLine = "";
old.Add(empty);
}
} else if (old.Count > cur.Count) {
int diff = old.Count - cur.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;
empty.Style = FontStyles.Normal;
empty.OldLine = "";
empty.NewLine = "";
cur.Add(empty);
}
}
}
private void AddSplitter(int column, double offset) {
var split = new Rectangle();
split.Width = 1;
split.Fill = FindResource("Brush.Border2") as Brush;
split.HorizontalAlignment = HorizontalAlignment.Left;
split.Margin = new Thickness(offset, 0, 0, 0);
split.SetValue(Grid.ColumnProperty, column);
textDiff.Children.Add(split);
splitters.Add(split);
}
private DataGrid CreateTextEditor(string[] lineNumbers) {
var grid = new DataGrid();
grid.EnableRowVirtualization = true;
grid.EnableColumnVirtualization = true;
2021-04-29 05:05:55 -07:00
grid.RowHeight = 16.0;
grid.FrozenColumnCount = lineNumbers.Length;
grid.ContextMenuOpening += OnTextDiffContextMenuOpening;
grid.RowStyle = FindResource("Style.DataGridRow.DiffViewer") as Style;
grid.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, (o, e) => {
var items = (o as DataGrid).SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var block = item as Block;
if (block == null) continue;
if (!block.IsContent) continue;
builder.Append(block.Content);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
}));
foreach (var number in lineNumbers) {
var colLineNumber = new DataGridTextColumn();
colLineNumber.IsReadOnly = true;
colLineNumber.Binding = new Binding(number);
colLineNumber.ElementStyle = FindResource("Style.TextBlock.LineNumber") as Style;
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, 12.0);
textContent.SetValue(TextBlock.MarginProperty, new Thickness(0));
textContent.SetValue(TextBlock.PaddingProperty, new Thickness(4, 0, 0, 0));
var visualTree = new FrameworkElementFactory(typeof(Grid));
visualTree.AppendChild(borderContent);
visualTree.AppendChild(textContent);
var colContent = new DataGridTemplateColumn();
colContent.CellTemplate = new DataTemplate();
colContent.CellTemplate.VisualTree = visualTree;
colContent.Width = DataGridLength.SizeToCells;
grid.Columns.Add(colContent);
return grid;
}
private double CalcLineNumberColWidth(string oldLine, string newLine) {
var number = oldLine;
if (newLine.Length > oldLine.Length) number = newLine;
var formatted = new FormattedText(
number,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
return formatted.Width + 16;
}
private void ClearCache() {
repo = null;
opt = null;
cachedTextChanges = null;
}
private T GetVisualChild<T>(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<T>(v);
}
if (child != null) {
break;
}
}
return child;
}
#endregion
#region EVENTS
private void OnDiffViewModeChanged(object sender, RoutedEventArgs e) {
if (editors.Count > 0) {
editors.Clear();
splitters.Clear();
textDiff.Children.Clear();
SetTextChange(seq);
2021-04-29 05:05:55 -07:00
}
}
private void OnTextDiffSizeChanged(object sender, SizeChangedEventArgs e) {
if (editors.Count == 0) return;
var total = textDiff.ActualWidth / editors.Count;
for (int i = 0; i < editors.Count; i++) {
var editor = editors[i];
var minWidth = total - editor.NonFrozenColumnsViewportHorizontalOffset;
if (editor.Items.Count * 16 > textDiff.ActualHeight) minWidth -= 8;
var lastColumn = editor.Columns.Count - 1;
editor.Columns[lastColumn].MinWidth = minWidth;
editor.Columns[lastColumn].Width = DataGridLength.SizeToCells;
editor.UpdateLayout();
}
}
private void OnTextDiffContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copyIcon = new Path();
copyIcon.Data = FindResource("Icon.Copy") as Geometry;
copyIcon.Width = 10;
var copy = new MenuItem();
copy.Header = App.Text("Diff.Copy");
copy.Icon = copyIcon;
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 Block;
if (block == null) continue;
if (!block.IsContent) continue;
builder.Append(block.Content);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void OnTextDiffSyncScroll(object sender, ScrollChangedEventArgs e) {
foreach (var editor in editors) {
var scroller = GetVisualChild<ScrollViewer>(editor);
if (scroller == null) continue;
if (e.VerticalChange != 0 && scroller.VerticalOffset != e.VerticalOffset) {
scroller.ScrollToVerticalOffset(e.VerticalOffset);
}
if (e.HorizontalChange != 0 && scroller.HorizontalOffset != e.HorizontalOffset) {
scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
private void OnTextDiffBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
private void GotoPrevChange(object sender, RoutedEventArgs e) {
if (editors.Count == 0) return;
var grid = editors[0];
var scroller = GetVisualChild<ScrollViewer>(grid);
if (scroller == null) return;
var firstVisible = (int)scroller.VerticalOffset;
var firstModeEnded = false;
var first = grid.Items[firstVisible] as Block;
for (int i = firstVisible - 1; i >= 0; i--) {
var next = grid.Items[i] as Block;
if (next.IsDifference) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break;
}
} else {
firstModeEnded = true;
}
}
}
private void GotoNextChange(object sender, RoutedEventArgs e) {
if (editors.Count == 0) return;
var grid = editors[0];
var scroller = GetVisualChild<ScrollViewer>(grid);
if (scroller == null) return;
var firstVisible = (int)scroller.VerticalOffset;
var firstModeEnded = false;
var first = grid.Items[firstVisible] as Block;
for (int i = firstVisible + 1; i < grid.Items.Count; i++) {
var next = grid.Items[i] as Block;
if (next.IsDifference) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break;
}
} else {
firstModeEnded = true;
}
}
}
#endregion
}
}