mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-11-01 13:13:21 -07:00
96d4150d26
* remove dotnet-tool.json because the project does not rely on any dotnet tools. * remove Directory.Build.props because the solution has only one project. * move src/SourceGit to src. It's not needed to put all sources into a subfolder of src since there's only one project.
376 lines
13 KiB
C#
376 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Media;
|
|
|
|
using AvaloniaEdit;
|
|
using AvaloniaEdit.Document;
|
|
using AvaloniaEdit.Editing;
|
|
using AvaloniaEdit.Rendering;
|
|
using AvaloniaEdit.TextMate;
|
|
using AvaloniaEdit.Utils;
|
|
|
|
namespace SourceGit.Views
|
|
{
|
|
public class BlameTextEditor : TextEditor
|
|
{
|
|
public class CommitInfoMargin : AbstractMargin
|
|
{
|
|
public CommitInfoMargin(BlameTextEditor editor)
|
|
{
|
|
_editor = editor;
|
|
ClipToBounds = true;
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
if (_editor.BlameData == null)
|
|
return;
|
|
|
|
var view = TextView;
|
|
if (view != null && view.VisualLinesValid)
|
|
{
|
|
var typeface = view.CreateTypeface();
|
|
var underlinePen = new Pen(Brushes.DarkOrange, 1);
|
|
|
|
foreach (var line in view.VisualLines)
|
|
{
|
|
var lineNumber = line.FirstDocumentLine.LineNumber;
|
|
if (lineNumber > _editor.BlameData.LineInfos.Count)
|
|
break;
|
|
|
|
var info = _editor.BlameData.LineInfos[lineNumber - 1];
|
|
var x = 0.0;
|
|
var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset;
|
|
if (!info.IsFirstInGroup && y > view.DefaultLineHeight * 0.6)
|
|
continue;
|
|
|
|
var shaLink = new FormattedText(
|
|
info.CommitSHA,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
Brushes.DarkOrange);
|
|
context.DrawText(shaLink, new Point(x, y));
|
|
context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2));
|
|
x += shaLink.Width + 8;
|
|
|
|
var time = new FormattedText(
|
|
info.Time,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
_editor.Foreground);
|
|
context.DrawText(time, new Point(x, y));
|
|
x += time.Width + 8;
|
|
|
|
var author = new FormattedText(
|
|
info.Author,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
_editor.Foreground);
|
|
context.DrawText(author, new Point(x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
var view = TextView;
|
|
var maxWidth = 0.0;
|
|
if (view != null && view.VisualLinesValid && _editor.BlameData != null)
|
|
{
|
|
var typeface = view.CreateTypeface();
|
|
var calculated = new HashSet<string>();
|
|
foreach (var line in view.VisualLines)
|
|
{
|
|
var lineNumber = line.FirstDocumentLine.LineNumber;
|
|
if (lineNumber > _editor.BlameData.LineInfos.Count)
|
|
break;
|
|
|
|
var info = _editor.BlameData.LineInfos[lineNumber - 1];
|
|
|
|
if (calculated.Contains(info.CommitSHA))
|
|
continue;
|
|
calculated.Add(info.CommitSHA);
|
|
|
|
var x = 0.0;
|
|
var shaLink = new FormattedText(
|
|
info.CommitSHA,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
Brushes.DarkOrange);
|
|
x += shaLink.Width + 8;
|
|
|
|
var time = new FormattedText(
|
|
info.Time,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
_editor.Foreground);
|
|
x += time.Width + 8;
|
|
|
|
var author = new FormattedText(
|
|
info.Author,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
_editor.Foreground);
|
|
x += author.Width;
|
|
|
|
if (maxWidth < x)
|
|
maxWidth = x;
|
|
}
|
|
}
|
|
|
|
return new Size(maxWidth, 0);
|
|
}
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
{
|
|
base.OnPointerPressed(e);
|
|
|
|
var view = TextView;
|
|
if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid)
|
|
{
|
|
var pos = e.GetPosition(this);
|
|
var typeface = view.CreateTypeface();
|
|
|
|
foreach (var line in view.VisualLines)
|
|
{
|
|
var lineNumber = line.FirstDocumentLine.LineNumber;
|
|
if (lineNumber >= _editor.BlameData.LineInfos.Count)
|
|
break;
|
|
|
|
var info = _editor.BlameData.LineInfos[lineNumber - 1];
|
|
var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset;
|
|
var shaLink = new FormattedText(
|
|
info.CommitSHA,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
typeface,
|
|
_editor.FontSize,
|
|
Brushes.DarkOrange);
|
|
|
|
var rect = new Rect(0, y, shaLink.Width, shaLink.Height);
|
|
if (rect.Contains(pos))
|
|
{
|
|
_editor.OnCommitSHAClicked(info.CommitSHA);
|
|
e.Handled = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly BlameTextEditor _editor = null;
|
|
}
|
|
|
|
public class VerticalSeperatorMargin : AbstractMargin
|
|
{
|
|
public VerticalSeperatorMargin(BlameTextEditor editor)
|
|
{
|
|
_editor = editor;
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
var pen = new Pen(_editor.BorderBrush, 1);
|
|
context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height));
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
return new Size(1, 0);
|
|
}
|
|
|
|
private readonly BlameTextEditor _editor = null;
|
|
}
|
|
|
|
public static readonly StyledProperty<Models.BlameData> BlameDataProperty =
|
|
AvaloniaProperty.Register<BlameTextEditor, Models.BlameData>(nameof(BlameData));
|
|
|
|
public Models.BlameData BlameData
|
|
{
|
|
get => GetValue(BlameDataProperty);
|
|
set => SetValue(BlameDataProperty, value);
|
|
}
|
|
|
|
protected override Type StyleKeyOverride => typeof(TextEditor);
|
|
|
|
public BlameTextEditor() : base(new TextArea(), new TextDocument())
|
|
{
|
|
IsReadOnly = true;
|
|
ShowLineNumbers = false;
|
|
WordWrap = false;
|
|
|
|
_textMate = Models.TextMateHelper.CreateForEditor(this);
|
|
|
|
TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) });
|
|
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
|
|
TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) });
|
|
TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this));
|
|
TextArea.TextView.ContextRequested += OnTextViewContextRequested;
|
|
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
|
|
TextArea.TextView.Margin = new Thickness(4, 0);
|
|
}
|
|
|
|
public void OnCommitSHAClicked(string sha)
|
|
{
|
|
if (DataContext is ViewModels.Blame blame)
|
|
{
|
|
blame.NavigateToCommit(sha);
|
|
}
|
|
}
|
|
|
|
protected override void OnUnloaded(RoutedEventArgs e)
|
|
{
|
|
base.OnUnloaded(e);
|
|
|
|
TextArea.LeftMargins.Clear();
|
|
TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
|
|
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
|
|
|
|
if (_textMate != null)
|
|
{
|
|
_textMate.Dispose();
|
|
_textMate = null;
|
|
}
|
|
}
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
if (change.Property == BlameDataProperty)
|
|
{
|
|
if (BlameData != null)
|
|
{
|
|
Models.TextMateHelper.SetGrammarByFileName(_textMate, BlameData.File);
|
|
Text = BlameData.Content;
|
|
}
|
|
else
|
|
{
|
|
Text = string.Empty;
|
|
}
|
|
}
|
|
else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null)
|
|
{
|
|
Models.TextMateHelper.SetThemeByApp(_textMate);
|
|
}
|
|
}
|
|
|
|
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
|
|
{
|
|
var selected = SelectedText;
|
|
if (string.IsNullOrEmpty(selected))
|
|
return;
|
|
|
|
var icon = new Avalonia.Controls.Shapes.Path();
|
|
icon.Width = 10;
|
|
icon.Height = 10;
|
|
icon.Stretch = Stretch.Uniform;
|
|
icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry;
|
|
|
|
var copy = new MenuItem();
|
|
copy.Header = App.Text("Copy");
|
|
copy.Icon = icon;
|
|
copy.Click += (o, ev) =>
|
|
{
|
|
App.CopyText(selected);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var menu = new ContextMenu();
|
|
menu.Items.Add(copy);
|
|
menu.Open(TextArea.TextView);
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
|
|
{
|
|
foreach (var margin in TextArea.LeftMargins)
|
|
{
|
|
if (margin is CommitInfoMargin commitInfo)
|
|
{
|
|
commitInfo.InvalidateMeasure();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private TextMate.Installation _textMate = null;
|
|
}
|
|
|
|
public partial class Blame : Window
|
|
{
|
|
public Blame()
|
|
{
|
|
if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
Owner = desktop.MainWindow;
|
|
}
|
|
|
|
InitializeComponent();
|
|
}
|
|
|
|
private void MaximizeOrRestoreWindow(object sender, TappedEventArgs e)
|
|
{
|
|
if (WindowState == WindowState.Maximized)
|
|
{
|
|
WindowState = WindowState.Normal;
|
|
}
|
|
else
|
|
{
|
|
WindowState = WindowState.Maximized;
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void CustomResizeWindow(object sender, PointerPressedEventArgs e)
|
|
{
|
|
if (sender is Border border)
|
|
{
|
|
if (border.Tag is WindowEdge edge)
|
|
{
|
|
BeginResizeDrag(edge, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BeginMoveWindow(object sender, PointerPressedEventArgs e)
|
|
{
|
|
BeginMoveDrag(e);
|
|
}
|
|
|
|
protected override void OnClosed(EventArgs e)
|
|
{
|
|
base.OnClosed(e);
|
|
GC.Collect();
|
|
}
|
|
|
|
private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.Blame blame)
|
|
{
|
|
var txt = sender as TextBlock;
|
|
blame.NavigateToCommit(txt.Text);
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|