feature: stage/unstage hunk (#265)

This commit is contained in:
leo 2024-07-17 16:56:16 +08:00
parent b9ed0987eb
commit b7e0e38de3
No known key found for this signature in database
18 changed files with 688 additions and 120 deletions

View file

@ -14,6 +14,11 @@ namespace SourceGit.Commands
public Diff(string repo, Models.DiffOption opt, int unified) public Diff(string repo, Models.DiffOption opt, int unified)
{ {
_result.TextDiff = new Models.TextDiff() {
Repo = repo,
Option = opt,
};
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"diff --ignore-cr-at-eol --unified={unified} {opt}"; Args = $"diff --ignore-cr-at-eol --unified={unified} {opt}";
@ -214,7 +219,7 @@ namespace SourceGit.Commands
} }
} }
private readonly Models.DiffResult _result = new Models.DiffResult() { TextDiff = new Models.TextDiff() }; private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>(); private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private int _oldLine = 0; private int _oldLine = 0;

View file

@ -66,6 +66,9 @@ namespace SourceGit.Models
public Vector SyncScrollOffset { get; set; } = Vector.Zero; public Vector SyncScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0; public int MaxLineNumber = 0;
public string Repo { get; set; } = null;
public DiffOption Option { get; set; } = null;
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output) public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
{ {
var isTracked = !string.IsNullOrEmpty(fileBlobGuid); var isTracked = !string.IsNullOrEmpty(fileBlobGuid);

View file

@ -307,6 +307,9 @@
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Find next match</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">Find next match</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Find previous match</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">Find previous match</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Open search panel</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">Open search panel</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">Stage Hunk</x:String>
<x:String x:Key="Text.Hunk.Unstage" xml:space="preserve">Unstage Hunk</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">Discard Hunk</x:String>
<x:String x:Key="Text.Init" xml:space="preserve">Initialize Repository</x:String> <x:String x:Key="Text.Init" xml:space="preserve">Initialize Repository</x:String>
<x:String x:Key="Text.Init.Path" xml:space="preserve">Path:</x:String> <x:String x:Key="Text.Init.Path" xml:space="preserve">Path:</x:String>
<x:String x:Key="Text.Init.Tip" xml:space="preserve">Invalid repository detected. Run `git init` under this path?</x:String> <x:String x:Key="Text.Init.Tip" xml:space="preserve">Invalid repository detected. Run `git init` under this path?</x:String>

View file

@ -310,6 +310,9 @@
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一个匹配搜索的位置</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一个匹配搜索的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一个匹配搜索的位置</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一个匹配搜索的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">打开搜索</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">打开搜索</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">暂存片断</x:String>
<x:String x:Key="Text.Hunk.Unstage" xml:space="preserve">移出暂存区</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">丢弃片断</x:String>
<x:String x:Key="Text.Init" xml:space="preserve">初始化新仓库</x:String> <x:String x:Key="Text.Init" xml:space="preserve">初始化新仓库</x:String>
<x:String x:Key="Text.Init.Path" xml:space="preserve">路径 </x:String> <x:String x:Key="Text.Init.Path" xml:space="preserve">路径 </x:String>
<x:String x:Key="Text.Init.Tip" xml:space="preserve">选择目录不是有效的Git仓库。是否需要在此目录执行`git init`操作?</x:String> <x:String x:Key="Text.Init.Tip" xml:space="preserve">选择目录不是有效的Git仓库。是否需要在此目录执行`git init`操作?</x:String>

View file

@ -310,6 +310,9 @@
<x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一個匹配搜尋的位置</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoNextMatch" xml:space="preserve">定位到下一個匹配搜尋的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一個匹配搜尋的位置</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.GotoPrevMatch" xml:space="preserve">定位到上一個匹配搜尋的位置</x:String>
<x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">開啟搜尋</x:String> <x:String x:Key="Text.Hotkeys.TextEditor.Search" xml:space="preserve">開啟搜尋</x:String>
<x:String x:Key="Text.Hunk.Stage" xml:space="preserve">暫存片斷</x:String>
<x:String x:Key="Text.Hunk.Unstage" xml:space="preserve">移出暫存區</x:String>
<x:String x:Key="Text.Hunk.Discard" xml:space="preserve">丟棄片斷</x:String>
<x:String x:Key="Text.Init" xml:space="preserve">初始化新倉庫</x:String> <x:String x:Key="Text.Init" xml:space="preserve">初始化新倉庫</x:String>
<x:String x:Key="Text.Init.Path" xml:space="preserve">路徑 </x:String> <x:String x:Key="Text.Init.Path" xml:space="preserve">路徑 </x:String>
<x:String x:Key="Text.Init.Tip" xml:space="preserve">選擇目錄不是有效的Git倉庫。是否需要在此目錄執行`git init`操作?</x:String> <x:String x:Key="Text.Init.Tip" xml:space="preserve">選擇目錄不是有效的Git倉庫。是否需要在此目錄執行`git init`操作?</x:String>

View file

@ -12,21 +12,6 @@ namespace SourceGit.ViewModels
{ {
public class DiffContext : ObservableObject public class DiffContext : ObservableObject
{ {
public string RepositoryPath
{
get => _repo;
}
public Models.Change WorkingCopyChange
{
get => _option.WorkingCopyChange;
}
public bool IsUnstaged
{
get => _option.IsUnstaged;
}
public string Title public string Title
{ {
get => _title; get => _title;

View file

@ -17,10 +17,17 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _syncScrollOffset, value); set => SetProperty(ref _syncScrollOffset, value);
} }
public Models.DiffOption Option
{
get;
set;
}
public TwoSideTextDiff(Models.TextDiff diff, TwoSideTextDiff previous = null) public TwoSideTextDiff(Models.TextDiff diff, TwoSideTextDiff previous = null)
{ {
File = diff.File; File = diff.File;
MaxLineNumber = diff.MaxLineNumber; MaxLineNumber = diff.MaxLineNumber;
Option = diff.Option;
foreach (var line in diff.Lines) foreach (var line in diff.Lines)
{ {

View file

@ -132,8 +132,7 @@
<!-- Changes --> <!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}"> <Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:ChangeCollectionView IsWorkingCopyChange="False" <v:ChangeCollectionView ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
Changes="{Binding VisibleChanges}" Changes="{Binding VisibleChanges}"
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"
ContextRequested="OnChangeContextRequested"/> ContextRequested="OnChangeContextRequested"/>

View file

@ -56,7 +56,7 @@
IsChecked="{Binding IsExpanded}" IsChecked="{Binding IsExpanded}"
IsVisible="{Binding IsFolder}"/> IsVisible="{Binding IsFolder}"/>
<v:ChangeStatusIcon Grid.Column="1" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/> <v:ChangeStatusIcon Grid.Column="1" Width="14" Height="14" IsUnstagedChange="{Binding #ThisControl.IsUnstagedChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/>
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/> <TextBlock Grid.Column="2" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
@ -75,7 +75,7 @@
<v:ChangeStatusIcon Grid.Column="0" <v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14" Width="14" Height="14"
Margin="4,0,0,0" Margin="4,0,0,0"
IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" IsUnstagedChange="{Binding #ThisControl.IsUnstagedChange}"
Change="{Binding}" /> Change="{Binding}" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
@ -104,7 +104,7 @@
<v:ChangeStatusIcon Grid.Column="0" <v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14" Width="14" Height="14"
Margin="4,0,0,0" Margin="4,0,0,0"
IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" IsUnstagedChange="{Binding #ThisControl.IsUnstagedChange}"
Change="{Binding}" /> Change="{Binding}" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"

View file

@ -40,13 +40,13 @@ namespace SourceGit.Views
public partial class ChangeCollectionView : UserControl public partial class ChangeCollectionView : UserControl
{ {
public static readonly StyledProperty<bool> IsWorkingCopyChangeProperty = public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<ChangeCollectionView, bool>(nameof(IsWorkingCopyChange)); AvaloniaProperty.Register<ChangeCollectionView, bool>(nameof(IsUnstagedChange));
public bool IsWorkingCopyChange public bool IsUnstagedChange
{ {
get => GetValue(IsWorkingCopyChangeProperty); get => GetValue(IsUnstagedChangeProperty);
set => SetValue(IsWorkingCopyChangeProperty, value); set => SetValue(IsUnstagedChangeProperty, value);
} }
public static readonly StyledProperty<SelectionMode> SelectionModeProperty = public static readonly StyledProperty<SelectionMode> SelectionModeProperty =

View file

@ -57,13 +57,13 @@ namespace SourceGit.Views
private static readonly string[] INDICATOR = ["?", "±", "+", "", "➜", "❏", "U", "★"]; private static readonly string[] INDICATOR = ["?", "±", "+", "", "➜", "❏", "U", "★"];
public static readonly StyledProperty<bool> IsWorkingCopyChangeProperty = public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<ChangeStatusIcon, bool>(nameof(IsWorkingCopyChange)); AvaloniaProperty.Register<ChangeStatusIcon, bool>(nameof(IsUnstagedChange));
public bool IsWorkingCopyChange public bool IsUnstagedChange
{ {
get => GetValue(IsWorkingCopyChangeProperty); get => GetValue(IsUnstagedChangeProperty);
set => SetValue(IsWorkingCopyChangeProperty, value); set => SetValue(IsUnstagedChangeProperty, value);
} }
public static readonly StyledProperty<Models.Change> ChangeProperty = public static readonly StyledProperty<Models.Change> ChangeProperty =
@ -77,7 +77,7 @@ namespace SourceGit.Views
static ChangeStatusIcon() static ChangeStatusIcon()
{ {
AffectsRender<ChangeStatusIcon>(IsWorkingCopyChangeProperty, ChangeProperty); AffectsRender<ChangeStatusIcon>(IsUnstagedChangeProperty, ChangeProperty);
} }
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
@ -89,7 +89,7 @@ namespace SourceGit.Views
IBrush background; IBrush background;
string indicator; string indicator;
if (IsWorkingCopyChange) if (IsUnstagedChange)
{ {
if (Change.IsConflit) if (Change.IsConflit)
{ {

View file

@ -45,8 +45,7 @@
<!-- Changes --> <!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}"> <Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:ChangeCollectionView IsWorkingCopyChange="False" <v:ChangeCollectionView SelectionMode="Single"
SelectionMode="Single"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
Changes="{Binding VisibleChanges}" Changes="{Binding VisibleChanges}"
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"

View file

@ -51,7 +51,6 @@
Width="14" Height="14" Width="14" Height="14"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="16,0,0,0" Margin="16,0,0,0"
IsWorkingCopyChange="False"
Change="{Binding}"/> Change="{Binding}"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding Path}" Margin="8,0" TextTrimming="CharacterEllipsis"/> <TextBlock Grid.Column="1" Classes="monospace" Text="{Binding Path}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</Grid> </Grid>

View file

@ -103,8 +103,7 @@
<!-- Changes --> <!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}"> <Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:ChangeCollectionView IsWorkingCopyChange="False" <v:ChangeCollectionView ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
Changes="{Binding VisibleChanges}" Changes="{Binding VisibleChanges}"
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"
ContextRequested="OnChangeContextRequested"/> ContextRequested="OnChangeContextRequested"/>

View file

@ -128,7 +128,7 @@
<DataGridTemplateColumn Header="ICON"> <DataGridTemplateColumn Header="ICON">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="False" Change="{Binding}"/> <v:ChangeStatusIcon Width="14" Height="14" Change="{Binding}"/>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>

View file

@ -9,29 +9,11 @@
x:Class="SourceGit.Views.TextDiffView" x:Class="SourceGit.Views.TextDiffView"
x:Name="ThisControl" x:Name="ThisControl"
Background="{DynamicResource Brush.Contents}"> Background="{DynamicResource Brush.Contents}">
<UserControl.DataTemplates> <Grid>
<DataTemplate DataType="m:TextDiff"> <ContentControl x:Name="Editor">
<v:CombinedTextDiffPresenter FileName="{Binding File}" <ContentControl.DataTemplates>
Foreground="{DynamicResource Brush.FG1}" <DataTemplate DataType="m:TextDiff">
LineBrush="{DynamicResource Brush.Border2}" <v:CombinedTextDiffPresenter FileName="{Binding File}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
IndicatorForeground="{DynamicResource Brush.FG2}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
/>
</DataTemplate>
<DataTemplate DataType="vm:TwoSideTextDiff">
<Grid ColumnDefinitions="*,1,*">
<v:SingleSideTextDiffPresenter Grid.Column="0"
IsOld="True"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}" Foreground="{DynamicResource Brush.FG1}"
LineBrush="{DynamicResource Brush.Border2}" LineBrush="{DynamicResource Brush.Border2}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}" EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
@ -44,27 +26,55 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}" WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
/> HighlightChunk="{Binding #ThisControl.HighlightChunk, Mode=TwoWay}"/>
</DataTemplate>
<Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/> <DataTemplate DataType="vm:TwoSideTextDiff">
<Grid ColumnDefinitions="*,1,*">
<v:SingleSideTextDiffPresenter Grid.Column="0"
IsOld="True"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}"
LineBrush="{DynamicResource Brush.Border2}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
IndicatorForeground="{DynamicResource Brush.FG2}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
HighlightChunk="{Binding #ThisControl.HighlightChunk, Mode=TwoWay}"/>
<v:SingleSideTextDiffPresenter Grid.Column="2" <Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
IsOld="False"
FileName="{Binding File}" <v:SingleSideTextDiffPresenter Grid.Column="2"
Foreground="{DynamicResource Brush.FG1}" IsOld="False"
LineBrush="{DynamicResource Brush.Border2}" FileName="{Binding File}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}" Foreground="{DynamicResource Brush.FG1}"
AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}" LineBrush="{DynamicResource Brush.Border2}"
DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}" EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}" AddedContentBackground="{DynamicResource Brush.Diff.AddedBG}"
DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}" DeletedContentBackground="{DynamicResource Brush.Diff.DeletedBG}"
IndicatorForeground="{DynamicResource Brush.FG2}" AddedHighlightBrush="{DynamicResource Brush.Diff.AddedHighlight}"
FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}" DeletedHighlightBrush="{DynamicResource Brush.Diff.DeletedHighlight}"
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}" IndicatorForeground="{DynamicResource Brush.FG2}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}" FontFamily="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}" UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
/> WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
</Grid> ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
</DataTemplate> HighlightChunk="{Binding #ThisControl.HighlightChunk, Mode=TwoWay}"/>
</UserControl.DataTemplates> </Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<StackPanel x:Name="Popup" IsVisible="False" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Effect="drop-shadow(0 0 6 #40000000)">
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Stage}" Click="OnStageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}"/>
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Unstage}" Click="OnUnstageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange, Converter={x:Static BoolConverters.Not}}"/>
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Discard}" Margin="8,0,0,0" Click="OnDiscardChunk"/>
</StackPanel>
</Grid>
</UserControl> </UserControl>

View file

@ -11,6 +11,7 @@ using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using AvaloniaEdit; using AvaloniaEdit;
@ -22,6 +23,14 @@ using AvaloniaEdit.Utils;
namespace SourceGit.Views namespace SourceGit.Views
{ {
public class TextViewHighlightChunk
{
public double Y { get; set; } = 0.0;
public double Height { get; set; } = 0.0;
public int StartIdx { get; set; } = 0;
public int EndIdx { get; set; } = 0;
}
public class ThemedTextDiffPresenter : TextEditor public class ThemedTextDiffPresenter : TextEditor
{ {
public static readonly StyledProperty<string> FileNameProperty = public static readonly StyledProperty<string> FileNameProperty =
@ -114,6 +123,15 @@ namespace SourceGit.Views
set => SetValue(ShowHiddenSymbolsProperty, value); set => SetValue(ShowHiddenSymbolsProperty, value);
} }
public static readonly StyledProperty<TextViewHighlightChunk> HighlightChunkProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, TextViewHighlightChunk>(nameof(HighlightChunk));
public TextViewHighlightChunk HighlightChunk
{
get => GetValue(HighlightChunkProperty);
set => SetValue(HighlightChunkProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextEditor); protected override Type StyleKeyOverride => typeof(TextEditor);
protected ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc) protected ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc)
@ -127,6 +145,30 @@ namespace SourceGit.Views
TextArea.TextView.Options.EnableEmailHyperlinks = false; TextArea.TextView.Options.EnableEmailHyperlinks = false;
} }
public override void Render(DrawingContext context)
{
base.Render(context);
var highlightChunk = HighlightChunk;
if (highlightChunk == null)
return;
var view = TextArea.TextView;
if (view == null || !view.VisualLinesValid)
return;
var color = (Color)this.FindResource("SystemAccentColor");
var brush = new SolidColorBrush(color, 0.5);
var pen = new Pen(color.ToUInt32());
var x = ((Point)view.TranslatePoint(new Point(0, 0), this)).X;
var rect = new Rect(x, highlightChunk.Y, view.Bounds.Width, highlightChunk.Height);
context.DrawRectangle(brush, null, rect);
context.DrawLine(pen, rect.TopLeft, rect.TopRight);
context.DrawLine(pen, rect.BottomLeft, rect.BottomRight);
}
protected override void OnLoaded(RoutedEventArgs e) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e); base.OnLoaded(e);
@ -166,6 +208,10 @@ namespace SourceGit.Views
{ {
Models.TextMateHelper.SetThemeByApp(_textMate); Models.TextMateHelper.SetThemeByApp(_textMate);
} }
else if (change.Property == HighlightChunkProperty)
{
InvalidateVisual();
}
} }
private void UpdateTextMate() private void UpdateTextMate()
@ -410,12 +456,16 @@ namespace SourceGit.Views
{ {
base.OnLoaded(e); base.OnLoaded(e);
TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.ContextRequested += OnTextViewContextRequested;
TextArea.TextView.PointerMoved += OnTextViewPointerMoved;
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
} }
protected override void OnUnloaded(RoutedEventArgs e) protected override void OnUnloaded(RoutedEventArgs e)
{ {
base.OnUnloaded(e); base.OnUnloaded(e);
TextArea.TextView.ContextRequested -= OnTextViewContextRequested; TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
TextArea.TextView.PointerMoved -= OnTextViewPointerMoved;
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
} }
protected override void OnDataContextChanged(EventArgs e) protected override void OnDataContextChanged(EventArgs e)
@ -475,6 +525,144 @@ namespace SourceGit.Views
TextArea.TextView.OpenContextMenu(menu); TextArea.TextView.OpenContextMenu(menu);
e.Handled = true; e.Handled = true;
} }
private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
{
if (DiffData.Option.WorkingCopyChange == null)
return;
if (!string.IsNullOrEmpty(SelectedText))
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
if (sender is TextView { VisualLinesValid: true } view)
{
var y = e.GetPosition(view).Y + view.VerticalOffset;
var lineIdx = -1;
foreach (var line in view.VisualLines)
{
var index = line.FirstDocumentLine.LineNumber;
if (index > DiffData.Lines.Count)
break;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.TextBottom);
if (endY > y)
{
lineIdx = index - 1;
break;
}
}
if (lineIdx == -1)
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var (startIdx, endIdx) = FindRangeByIndex(lineIdx);
if (startIdx == -1)
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var startLine = view.GetVisualLine(startIdx + 1);
var endLine = view.GetVisualLine(endIdx + 1);
var rectStartY = startLine != null ?
startLine.GetTextLineVisualYPosition(startLine.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset:
0;
var rectEndY = endLine != null ?
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset:
view.Bounds.Height;
var hightlight = new TextViewHighlightChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = startIdx,
EndIdx = endIdx,
};
SetCurrentValue(HighlightChunkProperty, hightlight);
}
}
private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
if (DiffData.Option.WorkingCopyChange == null)
return;
// The offset of TextView has not been updated here. Post a event to next frame.
Dispatcher.UIThread.Post(() => OnTextViewPointerMoved(sender, e));
}
private (int, int) FindRangeByIndex(int lineIdx)
{
var startIdx = -1;
var endIdx = -1;
var normalLineCount = 0;
var modifiedLineCount = 0;
var lines = DiffData.Lines;
for (int i = lineIdx; i >= 0; i--)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
startIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
startIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
normalLineCount = lines[lineIdx].Type == Models.TextDiffLineType.Normal ? 1 : 0;
for (int i = lineIdx + 1; i < lines.Count; i++)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
endIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
endIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
if (endIdx == -1)
endIdx = lines.Count - 1;
return modifiedLineCount > 0 ? (startIdx, endIdx) : (-1, -1);
}
} }
public class SingleSideTextDiffPresenter : ThemedTextDiffPresenter public class SingleSideTextDiffPresenter : ThemedTextDiffPresenter
@ -698,6 +886,8 @@ namespace SourceGit.Views
TextArea.PointerWheelChanged += OnTextAreaPointerWheelChanged; TextArea.PointerWheelChanged += OnTextAreaPointerWheelChanged;
TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.ContextRequested += OnTextViewContextRequested;
TextArea.TextView.PointerMoved += OnTextViewPointerMoved;
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
} }
protected override void OnUnloaded(RoutedEventArgs e) protected override void OnUnloaded(RoutedEventArgs e)
@ -712,6 +902,8 @@ namespace SourceGit.Views
TextArea.PointerWheelChanged -= OnTextAreaPointerWheelChanged; TextArea.PointerWheelChanged -= OnTextAreaPointerWheelChanged;
TextArea.TextView.ContextRequested -= OnTextViewContextRequested; TextArea.TextView.ContextRequested -= OnTextViewContextRequested;
TextArea.TextView.PointerMoved -= OnTextViewPointerMoved;
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
GC.Collect(); GC.Collect();
} }
@ -784,6 +976,152 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnTextViewPointerMoved(object sender, PointerEventArgs e)
{
if (DiffData.Option.WorkingCopyChange == null)
return;
if (!string.IsNullOrEmpty(SelectedText))
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var parentView = this.FindAncestorOfType<TextDiffView>();
if (parentView == null || parentView.DataContext == null)
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var textDiff = parentView.DataContext as Models.TextDiff;
if (sender is TextView { VisualLinesValid: true } view)
{
var y = e.GetPosition(view).Y + view.VerticalOffset;
var lineIdx = -1;
var lines = IsOld ? DiffData.Old : DiffData.New;
foreach (var line in view.VisualLines)
{
var index = line.FirstDocumentLine.LineNumber;
if (index > lines.Count)
break;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.TextBottom);
if (endY > y)
{
lineIdx = index - 1;
break;
}
}
if (lineIdx == -1)
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var (startIdx, endIdx) = FindRangeByIndex(lines, lineIdx);
if (startIdx == -1)
{
SetCurrentValue(HighlightChunkProperty, null);
return;
}
var startLine = view.GetVisualLine(startIdx + 1);
var endLine = view.GetVisualLine(endIdx + 1);
var rectStartY = startLine != null ?
startLine.GetTextLineVisualYPosition(startLine.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset :
0;
var rectEndY = endLine != null ?
endLine.GetTextLineVisualYPosition(endLine.TextLines[^1], VisualYPosition.TextBottom) - view.VerticalOffset :
view.Bounds.Height;
var hightlight = new TextViewHighlightChunk()
{
Y = rectStartY,
Height = rectEndY - rectStartY,
StartIdx = textDiff.Lines.IndexOf(lines[startIdx]),
EndIdx = textDiff.Lines.IndexOf(lines[endIdx]),
};
SetCurrentValue(HighlightChunkProperty, hightlight);
}
}
private void OnTextViewPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
if (DiffData.Option.WorkingCopyChange == null)
return;
// The offset of TextView has not been updated here. Post a event to next frame.
Dispatcher.UIThread.Post(() => OnTextViewPointerMoved(sender, e));
}
private (int, int) FindRangeByIndex(List<Models.TextDiffLine> lines, int lineIdx)
{
var startIdx = -1;
var endIdx = -1;
var normalLineCount = 0;
var modifiedLineCount = 0;
for (int i = lineIdx; i >= 0; i--)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
startIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
startIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
normalLineCount = lines[lineIdx].Type == Models.TextDiffLineType.Normal ? 1 : 0;
for (int i = lineIdx + 1; i < lines.Count; i++)
{
var line = lines[i];
if (line.Type == Models.TextDiffLineType.Indicator)
{
endIdx = i;
break;
}
if (line.Type == Models.TextDiffLineType.Normal)
{
normalLineCount++;
if (normalLineCount >= 2)
{
endIdx = i;
break;
}
}
else
{
normalLineCount = 0;
modifiedLineCount++;
}
}
if (endIdx == -1)
endIdx = lines.Count - 1;
return modifiedLineCount > 0 ? (startIdx, endIdx) : (-1, -1);
}
private ScrollViewer _scrollViewer = null; private ScrollViewer _scrollViewer = null;
} }
@ -798,6 +1136,24 @@ namespace SourceGit.Views
set => SetValue(UseSideBySideDiffProperty, value); set => SetValue(UseSideBySideDiffProperty, value);
} }
public static readonly StyledProperty<TextViewHighlightChunk> HighlightChunkProperty =
AvaloniaProperty.Register<TextDiffView, TextViewHighlightChunk>(nameof(HighlightChunk));
public TextViewHighlightChunk HighlightChunk
{
get => GetValue(HighlightChunkProperty);
set => SetValue(HighlightChunkProperty, value);
}
public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(IsUnstagedChange));
public bool IsUnstagedChange
{
get => GetValue(IsUnstagedChangeProperty);
set => SetValue(IsUnstagedChangeProperty, value);
}
static TextDiffView() static TextDiffView()
{ {
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) => UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
@ -807,11 +1163,24 @@ namespace SourceGit.Views
diff.SyncScrollOffset = Vector.Zero; diff.SyncScrollOffset = Vector.Zero;
if (v.UseSideBySideDiff) if (v.UseSideBySideDiff)
v.Content = new ViewModels.TwoSideTextDiff(diff); v.Editor.Content = new ViewModels.TwoSideTextDiff(diff);
else else
v.Content = diff; v.Editor.Content = diff;
} }
}); });
HighlightChunkProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
{
if (v.HighlightChunk == null)
{
v.Popup.IsVisible = false;
return;
}
var y = v.HighlightChunk.Y + 16;
v.Popup.Margin = new Thickness(0, y, 16, 0);
v.Popup.IsVisible = true;
});
} }
public TextDiffView() public TextDiffView()
@ -825,22 +1194,17 @@ namespace SourceGit.Views
if (diff == null) if (diff == null)
return; return;
var parentView = this.FindAncestorOfType<DiffView>(); var change = diff.Option.WorkingCopyChange;
if (parentView == null)
return;
var ctx = parentView.DataContext as ViewModels.DiffContext;
if (ctx == null)
return;
var change = ctx.WorkingCopyChange;
if (change == null) if (change == null)
return; return;
if (startLine > endLine) if (startLine > endLine)
(startLine, endLine) = (endLine, startLine); (startLine, endLine) = (endLine, startLine);
var selection = GetUnifiedSelection(diff, startLine, endLine, isOldSide); if (UseSideBySideDiff)
(startLine, endLine) = GetUnifiedRange(diff, startLine, endLine, isOldSide);
var selection = MakeSelection(diff, startLine, endLine, isOldSide);
if (!selection.HasChanges) if (!selection.HasChanges)
return; return;
@ -852,7 +1216,7 @@ namespace SourceGit.Views
if (workcopyView == null) if (workcopyView == null)
return; return;
if (ctx.IsUnstaged) if (diff.Option.IsUnstaged)
{ {
var stage = new MenuItem(); var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageSelectedLines"); stage.Header = App.Text("FileCM.StageSelectedLines");
@ -909,7 +1273,7 @@ namespace SourceGit.Views
if (repoView == null) if (repoView == null)
return; return;
if (ctx.IsUnstaged) if (diff.Option.IsUnstaged)
{ {
var stage = new MenuItem(); var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageSelectedLines"); stage.Header = App.Text("FileCM.StageSelectedLines");
@ -929,16 +1293,16 @@ namespace SourceGit.Views
} }
else if (!UseSideBySideDiff) else if (!UseSideBySideDiff)
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
diff.GeneratePatchFromSelection(change, treeGuid, selection, false, tmpFile); diff.GeneratePatchFromSelection(change, treeGuid, selection, false, tmpFile);
} }
else else
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, false, isOldSide, tmpFile); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, false, isOldSide, tmpFile);
} }
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--cache --index").Exec(); new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index").Exec();
File.Delete(tmpFile); File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually(); repo.MarkWorkingCopyDirtyManually();
@ -964,16 +1328,16 @@ namespace SourceGit.Views
} }
else if (!UseSideBySideDiff) else if (!UseSideBySideDiff)
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile); diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile);
} }
else else
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile);
} }
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--reverse").Exec(); new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--reverse").Exec();
File.Delete(tmpFile); File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually(); repo.MarkWorkingCopyDirtyManually();
@ -997,7 +1361,7 @@ namespace SourceGit.Views
repo.SetWatcherEnabled(false); repo.SetWatcherEnabled(false);
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result(); var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
var tmpFile = Path.GetTempFileName(); var tmpFile = Path.GetTempFileName();
if (change.Index == Models.ChangeState.Added) if (change.Index == Models.ChangeState.Added)
{ {
@ -1012,7 +1376,7 @@ namespace SourceGit.Views
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile);
} }
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--cache --index --reverse").Exec(); new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index --reverse").Exec();
File.Delete(tmpFile); File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually(); repo.MarkWorkingCopyDirtyManually();
@ -1032,22 +1396,21 @@ namespace SourceGit.Views
repo.SetWatcherEnabled(false); repo.SetWatcherEnabled(false);
var tmpFile = Path.GetTempFileName(); var tmpFile = Path.GetTempFileName();
if (change.WorkTree == Models.ChangeState.Untracked) var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
if (change.Index == Models.ChangeState.Added)
{ {
diff.GenerateNewPatchFromSelection(change, null, selection, true, tmpFile); diff.GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile);
} }
else if (!UseSideBySideDiff) else if (!UseSideBySideDiff)
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result();
diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile); diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile);
} }
else else
{ {
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result();
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, isOldSide, tmpFile);
} }
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--index --reverse").Exec(); new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--index --reverse").Exec();
File.Delete(tmpFile); File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually(); repo.MarkWorkingCopyDirtyManually();
@ -1067,26 +1430,211 @@ namespace SourceGit.Views
{ {
base.OnDataContextChanged(e); base.OnDataContextChanged(e);
if (HighlightChunk != null)
SetCurrentValue(HighlightChunkProperty, null);
var diff = DataContext as Models.TextDiff; var diff = DataContext as Models.TextDiff;
if (diff == null) if (diff == null)
{ {
Content = null; Editor.Content = null;
GC.Collect(); GC.Collect();
return; return;
} }
if (UseSideBySideDiff) if (UseSideBySideDiff)
Content = new ViewModels.TwoSideTextDiff(diff, Content as ViewModels.TwoSideTextDiff); Editor.Content = new ViewModels.TwoSideTextDiff(diff, Editor.Content as ViewModels.TwoSideTextDiff);
else else
Content = diff; Editor.Content = diff;
IsUnstagedChange = diff.Option.IsUnstaged;
} }
private Models.TextDiffSelection GetUnifiedSelection(Models.TextDiff diff, int startLine, int endLine, bool isOldSide) protected override void OnPointerExited(PointerEventArgs e)
{ {
var rs = new Models.TextDiffSelection(); base.OnPointerExited(e);
if (HighlightChunk != null)
SetCurrentValue(HighlightChunkProperty, null);
}
private void OnStageChunk(object sender, RoutedEventArgs e)
{
var chunk = HighlightChunk;
if (chunk == null)
return;
var diff = DataContext as Models.TextDiff;
if (diff == null)
return;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
var selection = MakeSelection(diff, chunk.StartIdx + 1, chunk.EndIdx + 1, false);
if (!selection.HasChanges)
return;
if (!selection.HasLeftChanges)
{
var workcopyView = this.FindAncestorOfType<WorkingCopy>();
if (workcopyView == null)
return;
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
workcopy?.StageChanges(new List<Models.Change> { change });
}
else
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView == null)
return;
var repo = repoView.DataContext as ViewModels.Repository;
if (repo == null)
return;
repo.SetWatcherEnabled(false);
var tmpFile = Path.GetTempFileName();
if (change.WorkTree == Models.ChangeState.Untracked)
{
diff.GenerateNewPatchFromSelection(change, null, selection, false, tmpFile);
}
else
{
var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
diff.GeneratePatchFromSelection(change, treeGuid, selection, false, tmpFile);
}
new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index").Exec();
File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually();
repo.SetWatcherEnabled(true);
}
}
private void OnUnstageChunk(object sender, RoutedEventArgs e)
{
var chunk = HighlightChunk;
if (chunk == null)
return;
var diff = DataContext as Models.TextDiff;
if (diff == null)
return;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
var selection = MakeSelection(diff, chunk.StartIdx + 1, chunk.EndIdx + 1, false);
if (!selection.HasChanges)
return;
// If all changes has been selected the use method provided by ViewModels.WorkingCopy.
// Otherwise, use `git apply`
if (!selection.HasLeftChanges)
{
var workcopyView = this.FindAncestorOfType<WorkingCopy>();
if (workcopyView == null)
return;
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
workcopy?.UnstageChanges(new List<Models.Change> { change });
}
else
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView == null)
return;
var repo = repoView.DataContext as ViewModels.Repository;
if (repo == null)
return;
repo.SetWatcherEnabled(false);
var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
var tmpFile = Path.GetTempFileName();
if (change.Index == Models.ChangeState.Added)
diff.GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile);
else
diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile);
new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--cache --index --reverse").Exec();
File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually();
repo.SetWatcherEnabled(true);
}
}
private void OnDiscardChunk(object sender, RoutedEventArgs e)
{
var chunk = HighlightChunk;
if (chunk == null)
return;
var diff = DataContext as Models.TextDiff;
if (diff == null)
return;
var change = diff.Option.WorkingCopyChange;
if (change == null)
return;
var selection = MakeSelection(diff, chunk.StartIdx + 1, chunk.EndIdx + 1, false);
if (!selection.HasChanges)
return;
// If all changes has been selected the use method provided by ViewModels.WorkingCopy.
// Otherwise, use `git apply`
if (!selection.HasLeftChanges)
{
var workcopyView = this.FindAncestorOfType<WorkingCopy>();
if (workcopyView == null)
return;
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
workcopy?.Discard(new List<Models.Change> { change }, diff.Option.IsUnstaged);
}
else
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView == null)
return;
var repo = repoView.DataContext as ViewModels.Repository;
if (repo == null)
return;
repo.SetWatcherEnabled(false);
var tmpFile = Path.GetTempFileName();
var treeGuid = new Commands.QueryStagedFileBlobGuid(diff.Repo, change.Path).Result();
if (change.Index == Models.ChangeState.Added)
{
diff.GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile);
}
else
{
diff.GeneratePatchFromSelection(change, treeGuid, selection, true, tmpFile);
}
new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", diff.Option.IsUnstaged ? "--reverse" : "--index --reverse").Exec();
File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually();
repo.SetWatcherEnabled(true);
}
}
private (int, int) GetUnifiedRange(Models.TextDiff diff, int startLine, int endLine, bool isOldSide)
{
endLine = Math.Min(endLine, diff.Lines.Count); endLine = Math.Min(endLine, diff.Lines.Count);
if (Content is ViewModels.TwoSideTextDiff twoSides) if (Editor.Content is ViewModels.TwoSideTextDiff twoSides)
{ {
var target = isOldSide ? twoSides.Old : twoSides.New; var target = isOldSide ? twoSides.Old : twoSides.New;
var firstContentLine = -1; var firstContentLine = -1;
@ -1101,7 +1649,7 @@ namespace SourceGit.Views
} }
if (firstContentLine < 0) if (firstContentLine < 0)
return rs; return (-1, -1);
var endContentLine = -1; var endContentLine = -1;
for (int i = Math.Min(endLine - 1, target.Count - 1); i >= startLine - 1; i--) for (int i = Math.Min(endLine - 1, target.Count - 1); i >= startLine - 1; i--)
@ -1115,7 +1663,7 @@ namespace SourceGit.Views
} }
if (endContentLine < 0) if (endContentLine < 0)
return rs; return (-1, -1);
var firstContent = target[firstContentLine]; var firstContent = target[firstContentLine];
var endContent = target[endContentLine]; var endContent = target[endContentLine];
@ -1123,6 +1671,12 @@ namespace SourceGit.Views
endLine = diff.Lines.IndexOf(endContent) + 1; endLine = diff.Lines.IndexOf(endContent) + 1;
} }
return (startLine, endLine);
}
private Models.TextDiffSelection MakeSelection(Models.TextDiff diff, int startLine, int endLine, bool isOldSide)
{
var rs = new Models.TextDiffSelection();
rs.StartLine = startLine; rs.StartLine = startLine;
rs.EndLine = endLine; rs.EndLine = endLine;

View file

@ -64,7 +64,7 @@
<!-- Unstaged Changes --> <!-- Unstaged Changes -->
<v:ChangeCollectionView Grid.Row="1" <v:ChangeCollectionView Grid.Row="1"
IsWorkingCopyChange="True" IsUnstagedChange="True"
SelectionMode="Multiple" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=UnstagedChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=UnstagedChangeViewMode}"
@ -98,7 +98,6 @@
<!-- Staged Changes --> <!-- Staged Changes -->
<v:ChangeCollectionView Grid.Row="3" <v:ChangeCollectionView Grid.Row="3"
IsWorkingCopyChange="False"
SelectionMode="Multiple" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}"