Compare commits

..

26 commits

Author SHA1 Message Date
Dmitrij D. Czarkoff
34b2451d1d
feature: make parents and children scrollable 2024-11-19 19:13:34 +01:00
Dmitrij D. Czarkoff
e8c8ac55a1
fix: hide children behind the preference 2024-11-19 19:01:09 +01:00
Dmitrij D. Czarkoff
f3c9690d47
fix: input lines may contain several commits
The first commit is always the immediate child, so take only 40 initial characters of the line
2024-11-19 18:38:36 +01:00
Dmitrij D. Czarkoff
88fb754415
feature: respect global commit limit for a good measure 2024-11-19 18:01:20 +01:00
Dmitrij D. Czarkoff
f96f602456
feature: execute children search asynchronously 2024-11-19 18:01:20 +01:00
Dmitrij D. Czarkoff
3fb1c763f3
feature: use repository filters to limit children search 2024-11-19 18:01:20 +01:00
Dmitrij D. Czarkoff
c611b62992
feature: add children list to the commit base info view
Useful for navigation between the commits.
2024-11-19 18:01:19 +01:00
leo
7a9c8d7444
ux: enable TextTrimming for author name in FileHistories
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 20:07:49 +08:00
aikawayataro
8021cd8566
enhance: introduce template engine for commit templates (#704) (#719) 2024-11-19 19:46:44 +08:00
leo
73687689ce
ux: min height of change block in minimap
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 16:45:07 +08:00
leo
b452e13453
localization: update Text.Repository.HistoriesOrder.ByDate
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 14:42:59 +08:00
leo
814529a690
feature: add hotkeys to stage/unstage/discard block or selected lines in text diff view (#718)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 12:12:54 +08:00
github-actions[bot]
00804e453e doc: Update translation status and missing keys 2024-11-19 03:33:55 +00:00
leo
b25f9bdb6c
feature: supports switch histories order mode (#705)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 11:32:13 +08:00
leo
f45bed6f92
fix: avoid NRE
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 10:31:17 +08:00
leo
3be90b2ef6
Merge branch 'master' into develop 2024-11-19 09:53:24 +08:00
leo
f7ef61f1ce
Merge branch 'release/8.39' 2024-11-19 09:53:03 +08:00
leo
0da46cb90b
version: Release 8.39
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 09:52:52 +08:00
leo
8b3d129890
code_review: PR #711
* SourceGit.Commands.* should not reference code in SourceGit.ViewModels.

Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 09:46:06 +08:00
Dmitrij D. Czarkoff
309db6e362
enhance: slightly improve statistics (#711)
* use preference MaxHistoryCommits
* use current culture to adjust first days of the week
2024-11-19 09:35:32 +08:00
leo
5b55e3530d
ux: better drop shadow effect for notifications
Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 09:34:09 +08:00
leo
d07a664166
code_review: PR #714
* remove `string.ToLower` warning
* override `OnLoaded` method directly
* clean namespace using

Signed-off-by: leo <longshuang@msn.cn>
2024-11-19 09:27:31 +08:00
Enner Pérez
ea1d966d27
feat: Reset Mode Hotkey (#714) 2024-11-19 09:14:53 +08:00
Dmitrij D. Czarkoff
f4618afee6
feature: switch WinMerge from 3-way to 2-way UI (#712)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Prepare version string (push) Waiting to run
2024-11-18 09:03:27 +08:00
leo
3b09ea45f5
feature: add change minimap for text diff view
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2024-11-17 21:49:33 +08:00
leo
13805f794a
Merge branch 'develop' 2024-11-11 10:29:09 +08:00
31 changed files with 898 additions and 240 deletions

View file

@ -47,7 +47,7 @@
## Translation Status
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-100.00%25-brightgreen)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-99.14%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-98.42%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-99.14%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md)
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.57%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-98.71%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-97.99%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-98.71%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-99.57%25-yellow)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md)
## How to Use

View file

@ -1,14 +1,16 @@
### de_DE.axaml: 100.00%
### de_DE.axaml: 99.57%
<details>
<summary>Missing Keys</summary>
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
</details>
### es_ES.axaml: 99.14%
### es_ES.axaml: 98.71%
<details>
@ -20,10 +22,13 @@
- Text.Repository.FilterCommits.Default
- Text.Repository.FilterCommits.Exclude
- Text.Repository.FilterCommits.Include
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
</details>
### fr_FR.axaml: 98.42%
### fr_FR.axaml: 97.99%
<details>
@ -39,11 +44,14 @@
- Text.Repository.FilterCommits.Default
- Text.Repository.FilterCommits.Exclude
- Text.Repository.FilterCommits.Include
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
- Text.ScanRepositories
</details>
### pt_BR.axaml: 99.14%
### pt_BR.axaml: 98.71%
<details>
@ -55,16 +63,21 @@
- Text.Repository.FilterCommits.Default
- Text.Repository.FilterCommits.Exclude
- Text.Repository.FilterCommits.Include
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
</details>
### ru_RU.axaml: 100.00%
### ru_RU.axaml: 99.57%
<details>
<summary>Missing Keys</summary>
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
</details>

View file

@ -1 +1 @@
8.38
8.39

View file

@ -19,7 +19,7 @@ namespace SourceGit.Commands
protected override void OnReadline(string line)
{
if (line.Contains(_commit))
_lines.Add(line);
_lines.Add(line.Substring(0, 40));
}
public IEnumerable<string> Result()

View file

@ -6,11 +6,13 @@ namespace SourceGit.Commands
{
public class QueryCommits : Command
{
public QueryCommits(string repo, string limits, bool needFindHead = true)
public QueryCommits(string repo, bool useTopoOrder, string limits, bool needFindHead = true)
{
var order = useTopoOrder ? "--topo-order" : "--date-order";
WorkingDirectory = repo;
Context = repo;
Args = "log --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + limits;
Args = $"log {order} --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {limits}";
_findFirstMerged = needFindHead;
}

View file

@ -4,11 +4,11 @@ namespace SourceGit.Commands
{
public class Statistics : Command
{
public Statistics(string repo)
public Statistics(string repo, int max)
{
WorkingDirectory = repo;
Context = repo;
Args = $"log --date-order --branches --remotes -40000 --pretty=format:\"%ct$%aN\"";
Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
}
public Models.Statistics Result()

View file

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
@ -9,9 +6,6 @@ namespace SourceGit.Models
{
public partial class CommitTemplate : ObservableObject
{
[GeneratedRegex(@"\$\{files(\:\d+)?\}")]
private static partial Regex REG_COMMIT_TEMPLATE_FILES();
public string Name
{
get => _name;
@ -26,55 +20,8 @@ namespace SourceGit.Models
public string Apply(Branch branch, List<Change> changes)
{
var content = _content
.Replace("${files_num}", $"{changes.Count}")
.Replace("${branch_name}", branch.Name);
var matches = REG_COMMIT_TEMPLATE_FILES().Matches(content);
if (matches.Count == 0)
return content;
var builder = new StringBuilder();
var last = 0;
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
if (!match.Success)
continue;
var start = match.Index;
if (start != last)
builder.Append(content.Substring(last, start - last));
var countStr = match.Groups[1].Value;
var paths = new List<string>();
var more = string.Empty;
if (countStr is { Length: <= 1 })
{
foreach (var c in changes)
paths.Add(c.Path);
}
else
{
var count = Math.Min(int.Parse(countStr.Substring(1)), changes.Count);
for (int j = 0; j < count; j++)
paths.Add(changes[j].Path);
if (count < changes.Count)
more = $" and {changes.Count - count} other files";
}
builder.Append(string.Join(", ", paths));
if (!string.IsNullOrEmpty(more))
builder.Append(more);
last = start + match.Length;
}
if (last != content.Length - 1)
builder.Append(content.Substring(last));
return builder.ToString();
var te = new TemplateEngine();
return te.Eval(_content, branch, changes);
}
private string _name = string.Empty;

View file

@ -39,7 +39,7 @@ namespace SourceGit.Models
new ExternalMerger(4, "tortoise_merge", "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""),
new ExternalMerger(5, "kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(6, "beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
};

View file

@ -6,23 +6,25 @@ namespace SourceGit.Models
{
public static readonly ResetMode[] Supported =
[
new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green),
new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Orange),
new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", Brushes.Purple),
new ResetMode("Keep", "Reset while keeping local modifications", "--keep", Brushes.Purple),
new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red),
new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", "S", Brushes.Green),
new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", "M", Brushes.Orange),
new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", "G", Brushes.Purple),
new ResetMode("Keep", "Reset while keeping local modifications", "--keep", "K", Brushes.Purple),
new ResetMode("Hard", "Discard all changes", "--hard", "H", Brushes.Red),
];
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public string Key { get; set; }
public IBrush Color { get; set; }
public ResetMode(string n, string d, string a, IBrush b)
public ResetMode(string n, string d, string a, string k, IBrush b)
{
Name = n;
Desc = d;
Arg = a;
Key = k;
Color = b;
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using LiveChartsCore;
using LiveChartsCore.Defaults;
@ -138,7 +139,8 @@ namespace SourceGit.Models
public Statistics()
{
_today = DateTime.Now.ToLocalTime().Date;
_thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24);
var weekOffset = (7 + (int)_today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7;
_thisWeekStart = _today.AddDays(-weekOffset);
_thisMonthStart = _today.AddDays(1 - _today.Day);
All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue);

View file

@ -0,0 +1,410 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace SourceGit.Models
{
public class TemplateEngine
{
private class Context(Branch branch, IReadOnlyList<Change> changes)
{
public Branch branch = branch;
public IReadOnlyList<Change> changes = changes;
}
private class Text(string text)
{
public string text = text;
}
private class Variable(string name)
{
public string name = name;
}
private class SlicedVariable(string name, int count)
{
public string name = name;
public int count = count;
}
private class RegexVariable(string name, Regex regex, string replacement)
{
public string name = name;
public Regex regex = regex;
public string replacement = replacement;
}
private const char ESCAPE = '\\';
private const char VARIABLE_ANCHOR = '$';
private const char VARIABLE_START = '{';
private const char VARIABLE_END = '}';
private const char VARIABLE_SLICE = ':';
private const char VARIABLE_REGEX = '/';
private const char NEWLINE = '\n';
private const RegexOptions REGEX_OPTIONS = RegexOptions.Singleline | RegexOptions.IgnoreCase;
public string Eval(string text, Branch branch, IReadOnlyList<Change> changes)
{
Reset();
_chars = text.ToCharArray();
Parse();
var context = new Context(branch, changes);
var sb = new StringBuilder();
sb.EnsureCapacity(text.Length);
foreach (var token in _tokens)
{
switch (token)
{
case Text text_token:
sb.Append(text_token.text);
break;
case Variable var_token:
sb.Append(EvalVariable(context, var_token));
break;
case SlicedVariable sliced_var:
sb.Append(EvalVariable(context, sliced_var));
break;
case RegexVariable regex_var:
sb.Append(EvalVariable(context, regex_var));
break;
}
}
return sb.ToString();
}
private void Reset()
{
_pos = 0;
_chars = [];
_tokens.Clear();
}
private char? Next()
{
var c = Peek();
if (c is not null)
{
_pos++;
}
return c;
}
private char? Peek()
{
return (_pos >= _chars.Length) ? null : _chars[_pos];
}
private int? Integer()
{
var start = _pos;
while (Peek() is char c && c >= '0' && c <= '9')
{
_pos++;
}
if (start >= _pos)
return null;
var chars = new ReadOnlySpan<char>(_chars, start, _pos - start);
return int.Parse(chars);
}
private void Parse()
{
// text token start
var tok = _pos;
bool esc = false;
while (Next() is char c)
{
if (esc)
{
esc = false;
continue;
}
switch (c)
{
case ESCAPE:
// allow to escape only \ and $
if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR))
{
esc = true;
FlushText(tok, _pos - 1);
tok = _pos;
}
break;
case VARIABLE_ANCHOR:
// backup the position
var bak = _pos;
var variable = TryParseVariable();
if (variable is null)
{
// no variable found, rollback
_pos = bak;
}
else
{
// variable found, flush a text token
FlushText(tok, bak - 1);
_tokens.Add(variable);
tok = _pos;
}
break;
}
}
// flush text token
FlushText(tok, _pos);
}
private void FlushText(int start, int end)
{
int len = end - start;
if (len <= 0)
return;
var text = new string(_chars, start, len);
_tokens.Add(new Text(text));
}
private object TryParseVariable()
{
if (Next() != VARIABLE_START)
return null;
int name_start = _pos;
while (Next() is char c)
{
// name character, continue advancing
if (IsNameChar(c))
continue;
var name_end = _pos - 1;
// not a name character but name is empty, cancel
if (name_start >= name_end)
return null;
var name = new string(_chars, name_start, name_end - name_start);
return c switch
{
// variable
VARIABLE_END => new Variable(name),
// sliced variable
VARIABLE_SLICE => TryParseSlicedVariable(name),
// regex variable
VARIABLE_REGEX => TryParseRegexVariable(name),
_ => null,
};
}
return null;
}
private object TryParseSlicedVariable(string name)
{
int? n = Integer();
if (n is null)
return null;
if (Next() != VARIABLE_END)
return null;
return new SlicedVariable(name, (int)n);
}
private object TryParseRegexVariable(string name)
{
var regex = ParseRegex();
if (regex == null)
return null;
var replacement = ParseReplacement();
if (replacement == null)
return null;
return new RegexVariable(name, regex, replacement);
}
private Regex ParseRegex()
{
var sb = new StringBuilder();
var tok = _pos;
var esc = false;
while (Next() is char c)
{
if (esc)
{
esc = false;
continue;
}
switch (c)
{
case ESCAPE:
// allow to escape only / as \ and { used frequently in regexes
if (Peek() == VARIABLE_REGEX)
{
esc = true;
sb.Append(_chars, tok, _pos - 1 - tok);
tok = _pos;
}
break;
case VARIABLE_REGEX:
// goto is fine
goto Loop_exit;
case NEWLINE:
// no newlines allowed
return null;
}
}
Loop_exit:
sb.Append(_chars, tok, _pos - 1 - tok);
try
{
var pattern = sb.ToString();
if (pattern.Length == 0)
return null;
var regex = new Regex(pattern, REGEX_OPTIONS);
return regex;
}
catch (RegexParseException)
{
return null;
}
}
private string ParseReplacement()
{
var sb = new StringBuilder();
var tok = _pos;
var esc = false;
while (Next() is char c)
{
if (esc)
{
esc = false;
continue;
}
switch (c)
{
case ESCAPE:
// allow to escape only }
if (Peek() == VARIABLE_END)
{
esc = true;
sb.Append(_chars, tok, _pos - 1 - tok);
tok = _pos;
}
break;
case VARIABLE_END:
// goto is fine
goto Loop_exit;
case NEWLINE:
// no newlines allowed
return null;
}
}
Loop_exit:
sb.Append(_chars, tok, _pos - 1 - tok);
var replacement = sb.ToString();
return replacement;
}
private static bool IsNameChar(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
// (?) notice or log if variable is not found
private static string EvalVariable(Context context, string name)
{
if (!s_variables.TryGetValue(name, out var getter))
{
return string.Empty;
}
return getter(context);
}
private static string EvalVariable(Context context, Variable variable)
{
return EvalVariable(context, variable.name);
}
private static string EvalVariable(Context context, SlicedVariable variable)
{
if (!s_slicedVariables.TryGetValue(variable.name, out var getter))
{
return string.Empty;
}
return getter(context, variable.count);
}
private static string EvalVariable(Context context, RegexVariable variable)
{
var str = EvalVariable(context, variable.name);
if (string.IsNullOrEmpty(str))
return str;
return variable.regex.Replace(str, variable.replacement);
}
private int _pos = 0;
private char[] _chars = [];
private readonly List<object> _tokens = [];
private delegate string VariableGetter(Context context);
private static readonly IReadOnlyDictionary<string, VariableGetter> s_variables = new Dictionary<string, VariableGetter>() {
// legacy variables
{"branch_name", GetBranchName},
{"files_num", GetFilesCount},
{"files", GetFiles},
//
{"BRANCH", GetBranchName},
{"FILES_COUNT", GetFilesCount},
{"FILES", GetFiles},
};
private static string GetBranchName(Context context)
{
return context.branch.Name;
}
private static string GetFilesCount(Context context)
{
return context.changes.Count.ToString();
}
private static string GetFiles(Context context)
{
var paths = new List<string>();
foreach (var c in context.changes)
paths.Add(c.Path);
return string.Join(", ", paths);
}
private delegate string VariableSliceGetter(Context context, int count);
private static readonly IReadOnlyDictionary<string, VariableSliceGetter> s_slicedVariables = new Dictionary<string, VariableSliceGetter>() {
// legacy variables
{"files", GetFilesSliced},
//
{"FILES", GetFilesSliced},
};
private static string GetFilesSliced(Context context, int count)
{
var sb = new StringBuilder();
var paths = new List<string>();
var max = Math.Min(count, context.changes.Count);
for (int i = 0; i < max; i++)
paths.Add(context.changes[i].Path);
sb.AppendJoin(", ", paths);
if (max < context.changes.Count)
sb.AppendFormat(" and {0} other files", context.changes.Count - max);
return sb.ToString();
}
}
}

View file

@ -79,6 +79,7 @@
<StreamGeometry x:Key="Icons.Move">M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z</StreamGeometry>
<StreamGeometry x:Key="Icons.MoveToAnotherGroup">M64 363l0 204 265 0L329 460c0-11 6-18 14-20C349 437 355 437 362 441c93 60 226 149 226 149 33 22 34 60 0 82 0 0-133 89-226 149-14 9-32-3-32-18l-1-110L64 693l0 117c0 41 34 75 75 75l746 0c41 0 75-34 75-74L960 364c0-0 0-1 0-1L64 363zM64 214l0 75 650 0-33-80c-16-38-62-69-103-69l-440 0C97 139 64 173 64 214z</StreamGeometry>
<StreamGeometry x:Key="Icons.OpenWith">M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z</StreamGeometry>
<StreamGeometry x:Key="Icons.Order">M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z</StreamGeometry>
<StreamGeometry x:Key="Icons.Password">M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.Paste">M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Plus">m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z</StreamGeometry>

View file

@ -452,6 +452,7 @@
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Language</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">History Commits</x:String>
<x:String x:Key="Text.Preference.General.ShowAuthorTime" xml:space="preserve">Show author time intead of commit time in graph</x:String>
<x:String x:Key="Text.Preference.General.ShowChildren" xml:space="preserve">Show children in the comment details</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Subject Guide Length</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.CRLF" xml:space="preserve">Enable Auto CRLF</x:String>
@ -544,6 +545,9 @@
<x:String x:Key="Text.Repository.FilterCommits.Default" xml:space="preserve">Unset</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Exclude" xml:space="preserve">Hide in commit graph</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Include" xml:space="preserve">Filter in commit graph</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder" xml:space="preserve">Switch Order Mode</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.ByDate" xml:space="preserve">Commit Date (--date-order)</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.Topo" xml:space="preserve">Topologically (--topo-order)</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">LOCAL BRANCHES</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">Navigate to HEAD</x:String>
<x:String x:Key="Text.Repository.FirstParentFilterToggle" xml:space="preserve">Enable '--first-parent' Option</x:String>

View file

@ -547,6 +547,9 @@
<x:String x:Key="Text.Repository.FilterCommits.Default" xml:space="preserve">不指定</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Exclude" xml:space="preserve">在提交列表中隐藏</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Include" xml:space="preserve">使用其对提交列表过滤</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder" xml:space="preserve">切换排序模式</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.ByDate" xml:space="preserve">按提交时间 (--date-order)</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.Topo" xml:space="preserve">按拓扑排序 (--topo-order)</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">本地分支</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">定位HEAD</x:String>
<x:String x:Key="Text.Repository.FirstParentFilterToggle" xml:space="preserve">启用 --first-parent 过滤选项</x:String>

View file

@ -546,6 +546,9 @@
<x:String x:Key="Text.Repository.FilterCommits.Default" xml:space="preserve">不指定</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Exclude" xml:space="preserve">在提交清單中隱藏</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Include" xml:space="preserve">使用其來篩選提交清單</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder" xml:space="preserve">切換排序方式</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.ByDate" xml:space="preserve">按提交时间排序 (--date-order)</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.Topo" xml:space="preserve">按拓扑排序 (--topo-order)</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">本機分支</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">回到 HEAD</x:String>
<x:String x:Key="Text.Repository.FirstParentFilterToggle" xml:space="preserve">啟用 [--first-parent] 選項</x:String>

View file

@ -543,6 +543,8 @@ namespace SourceGit.ViewModels
_cancelToken = new Commands.Command.CancelToken();
if (Preference.Instance.ShowChildren)
{
Task.Run(() =>
{
var cmdChildren = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, _repo.Settings.BuildHistoriesFilter()) { Cancel = _cancelToken };
@ -550,6 +552,7 @@ namespace SourceGit.ViewModels
if (!cmdChildren.Cancel.Requested)
Dispatcher.UIThread.Post(() => Children.AddRange(children));
});
}
Task.Run(() =>
{

View file

@ -64,7 +64,8 @@ namespace SourceGit.ViewModels
Task.Run(() =>
{
var commits = new Commands.QueryCommits(_repo.FullPath, $"-n 10000 {commit} -- \"{file}\"", false).Result();
var based = commit ?? string.Empty;
var commits = new Commands.QueryCommits(_repo.FullPath, false, $"-n 10000 {based} -- \"{file}\"", false).Result();
Dispatcher.UIThread.Invoke(() =>
{
IsLoading = false;

View file

@ -294,6 +294,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _statisticsSampleColor, value);
}
public bool ShowChildren
{
get => _showChildren;
set => SetProperty(ref _showChildren, value);
}
public List<RepositoryNode> RepositoryNodes
{
get;
@ -617,5 +623,7 @@ namespace SourceGit.ViewModels
private string _externalMergeToolPath = string.Empty;
private uint _statisticsSampleColor = 0xFF00FF00;
private bool _showChildren = false;
}
}

View file

@ -106,6 +106,16 @@ namespace SourceGit.ViewModels
}
}
public bool EnableTopoOrderInHistories
{
get => _enableTopoOrderInHistories;
set
{
if (SetProperty(ref _enableTopoOrderInHistories, value))
Task.Run(RefreshCommits);
}
}
public string Filter
{
get => _filter;
@ -852,7 +862,7 @@ namespace SourceGit.ViewModels
else
builder.Append(filters);
var commits = new Commands.QueryCommits(_fullpath, builder.ToString()).Result();
var commits = new Commands.QueryCommits(_fullpath, _enableTopoOrderInHistories, builder.ToString()).Result();
var graph = Models.CommitGraph.Parse(commits, _enableFirstParentInHistories);
Dispatcher.UIThread.Invoke(() =>
@ -2228,6 +2238,7 @@ namespace SourceGit.ViewModels
private bool _onlySearchCommitsInCurrentBranch = false;
private bool _enableReflog = false;
private bool _enableFirstParentInHistories = false;
private bool _enableTopoOrderInHistories = false;
private string _searchCommitFilter = string.Empty;
private List<Models.Commit> _searchedCommits = new List<Models.Commit>();
private List<string> _revisionFiles = new List<string>();

View file

@ -54,7 +54,7 @@ namespace SourceGit.ViewModels
{
Task.Run(() =>
{
var result = new Commands.Statistics(repo).Result();
var result = new Commands.Statistics(repo, Preference.Instance.MaxHistoryCommits).Result();
Dispatcher.UIThread.Invoke(() =>
{
_data = result;

View file

@ -102,7 +102,8 @@
<!-- PARENTS -->
<TextBlock Grid.Row="1" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Parents}" IsVisible="{Binding Parents.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<ItemsControl Grid.Row="1" Grid.Column="1" Height="24" Margin="12,0,0,0" ItemsSource="{Binding Parents}" IsVisible="{Binding Parents.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden" AllowAutoHide="True">
<ItemsControl Height="24" Margin="12,0,0,0" ItemsSource="{Binding Parents}" IsVisible="{Binding Parents.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
@ -142,10 +143,12 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- CHILDREN -->
<TextBlock Grid.Row="2" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Children}" IsVisible="{Binding #ThisControl.Children.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}"/>
<ItemsControl Grid.Row="2" Grid.Column="1" Height="24" Margin="12,0,0,0" ItemsSource="{Binding #ThisControl.Children}" IsVisible="{Binding #ThisControl.Children.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ScrollViewer Grid.Row="2" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden" AllowAutoHide="True">
<ItemsControl Height="24" Margin="12,0,0,0" ItemsSource="{Binding #ThisControl.Children}" IsVisible="{Binding #ThisControl.Children.Count, Converter={x:Static c:IntConverters.IsGreaterThanZero}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
@ -185,6 +188,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- REFS -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Refs}" IsVisible="{Binding HasDecorators}"/>

View file

@ -38,7 +38,7 @@ namespace SourceGit.Views
}
public static readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<CommitRefsPresenter, IBrush>(nameof(Background), null);
AvaloniaProperty.Register<CommitRefsPresenter, IBrush>(nameof(Background), Brushes.Transparent);
public IBrush Background
{
@ -56,7 +56,7 @@ namespace SourceGit.Views
}
public static readonly StyledProperty<bool> UseGraphColorProperty =
AvaloniaProperty.Register<CommitRefsPresenter, bool>(nameof(UseGraphColor), false);
AvaloniaProperty.Register<CommitRefsPresenter, bool>(nameof(UseGraphColor));
public bool UseGraphColor
{
@ -96,7 +96,6 @@ namespace SourceGit.Views
var x = 1.0;
foreach (var item in _items)
{
var iconRect = new RoundedRect(new Rect(x, 0, 16, 16), new CornerRadius(2, 0, 0, 2));
var entireRect = new RoundedRect(new Rect(x, 0, item.Width, 16), new CornerRadius(2));
if (item.IsHead)

View file

@ -80,7 +80,7 @@
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto">
<v:Avatar Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" IsHitTestVisible="False" User="{Binding Author}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Author.Name}" Margin="8,0,0,0" ClipToBounds="True"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding Author.Name}" Margin="8,0,0,0" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="2"
Classes="primary"
Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"

View file

@ -109,7 +109,7 @@
<ItemsControl ItemsSource="{Binding Notifications}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="m:Notification">
<Border Margin="6" HorizontalAlignment="Stretch" VerticalAlignment="Top" Effect="drop-shadow(0 0 12 #A0000000)">
<Border Margin="6" HorizontalAlignment="Stretch" VerticalAlignment="Top" Effect="drop-shadow(0 0 8 #8F000000)">
<Border Padding="8" CornerRadius="6" Background="{DynamicResource Brush.Popup}">
<Grid RowDefinitions="26,Auto">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="8,0">

View file

@ -45,7 +45,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.General}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.General.Locale}"
HorizontalAlignment="Right"
@ -114,6 +114,11 @@
Height="32"
Content="{DynamicResource Text.Preference.General.Check4UpdatesOnStartup}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=Check4UpdatesOnStartup, Mode=TwoWay}"/>
<CheckBox Grid.Row="6" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preference.General.ShowChildren}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowChildren, Mode=TwoWay}"/>
</Grid>
</TabItem>

View file

@ -67,7 +67,7 @@
</ListBox.ItemsPanel>
<ListBoxItem>
<Grid Classes="view_mode" ColumnDefinitions="32,*,Auto,Auto,Auto">
<Grid Classes="view_mode" ColumnDefinitions="32,*,Auto,Auto,Auto,Auto">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Histories}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{DynamicResource Text.Histories}"/>
<ToggleButton Grid.Column="2"
@ -91,6 +91,13 @@
ToolTip.Tip="{DynamicResource Text.Repository.FirstParentFilterToggle}">
<Path Width="12" Height="12" Data="{StaticResource Icons.FirstParentFilter}"/>
</ToggleButton>
<Button Grid.Column="5"
Classes="icon_button"
Width="28" Height="26"
Click="OnSwitchHistoriesOrderClicked"
ToolTip.Tip="{DynamicResource Text.Repository.HistoriesOrder}">
<Path Width="12" Height="12" Margin="0,2,0,0" Data="{StaticResource Icons.Order}"/>
</Button>
</Grid>
</ListBoxItem>

View file

@ -395,5 +395,38 @@ namespace SourceGit.Views
}
e.Handled = true;
}
private void OnSwitchHistoriesOrderClicked(object sender, RoutedEventArgs e)
{
if (sender is Button button && DataContext is ViewModels.Repository repo)
{
var checkIcon = App.CreateMenuIcon("Icons.Check");
var dateOrder = new MenuItem();
dateOrder.Header = App.Text("Repository.HistoriesOrder.ByDate");
dateOrder.Icon = repo.EnableTopoOrderInHistories ? null : checkIcon;
dateOrder.Click += (_, ev) =>
{
repo.EnableTopoOrderInHistories = false;
ev.Handled = true;
};
var topoOrder = new MenuItem();
topoOrder.Header = App.Text("Repository.HistoriesOrder.Topo");
topoOrder.Icon = repo.EnableTopoOrderInHistories ? checkIcon : null;
topoOrder.Click += (_, ev) =>
{
repo.EnableTopoOrderInHistories = true;
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(dateOrder);
menu.Items.Add(topoOrder);
menu.Open(button);
}
e.Handled = true;
}
}
}

View file

@ -37,16 +37,19 @@
Margin="0,0,8,0"
Text="{DynamicResource Text.Reset.Mode}"/>
<ComboBox Grid.Row="2" Grid.Column="1"
x:Name="ResetMode"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={x:Static m:ResetMode.Supported}}"
SelectedItem="{Binding SelectedMode, Mode=TwoWay}">
SelectedItem="{Binding SelectedMode, Mode=TwoWay}"
KeyDown="OnResetModeKeyDown">
<ComboBox.ItemTemplate>
<DataTemplate DataType="m:ResetMode">
<Grid ColumnDefinitions="16,60,*">
<Ellipse Grid.Column="0" Width="12" Height="12" Fill="{Binding Color}"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding Desc}" FontSize="11" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="2,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding Desc}" Margin="2,0,16,0" FontSize="11" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="3" Text="{Binding Key}" FontSize="11" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>

View file

@ -1,4 +1,6 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
@ -8,5 +10,29 @@ namespace SourceGit.Views
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
ResetMode.Focus();
}
private void OnResetModeKeyDown(object sender, KeyEventArgs e)
{
if (sender is ComboBox comboBox)
{
var key = e.Key.ToString();
for (int i = 0; i < Models.ResetMode.Supported.Length; i++)
{
if (key.Equals(Models.ResetMode.Supported[i].Key, System.StringComparison.OrdinalIgnoreCase))
{
comboBox.SelectedIndex = i;
e.Handled = true;
return;
}
}
}
}
}
}

View file

@ -13,7 +13,10 @@
<ContentControl x:Name="Editor">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:TextDiff">
<v:CombinedTextDiffPresenter FileName="{Binding File}"
<Grid ColumnDefinitions="*,1,8">
<v:CombinedTextDiffPresenter Grid.Column="0"
x:Name="CombinedPresenter"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}"
LineBrush="{DynamicResource Brush.Border2}"
EmptyContentBackground="{DynamicResource Brush.Diff.EmptyBG}"
@ -29,11 +32,20 @@
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
<Rectangle Grid.Column="1" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<v:TextDiffViewMinimap Grid.Column="2"
DisplayRange="{Binding #CombinedPresenter.DisplayRange}"
AddedLineBrush="{DynamicResource Brush.Diff.AddedBG}"
DeletedLineBrush="{DynamicResource Brush.Diff.DeletedBG}"/>
</Grid>
</DataTemplate>
<DataTemplate DataType="vm:TwoSideTextDiff">
<Grid ColumnDefinitions="*,1,*">
<Grid ColumnDefinitions="*,1,*,1,12">
<v:SingleSideTextDiffPresenter Grid.Column="0"
x:Name="LeftSidePresenter"
IsOld="True"
FileName="{Binding File}"
Foreground="{DynamicResource Brush.FG1}"
@ -72,15 +84,42 @@
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
<Rectangle Grid.Column="3" Fill="{DynamicResource Brush.Border2}" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<v:TextDiffViewMinimap Grid.Column="4"
DisplayRange="{Binding #LeftSidePresenter.DisplayRange}"
AddedLineBrush="{DynamicResource Brush.Diff.AddedBG}"
DeletedLineBrush="{DynamicResource Brush.Diff.DeletedBG}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<StackPanel x:Name="Popup" IsVisible="False" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Effect="drop-shadow(0 0 8 #80000000)">
<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" IsVisible="{Binding #ThisControl.IsUnstagedChange}"/>
<Button Classes="flat" Click="OnStageChunk" HotKey="{OnPlatform Ctrl+S, macOS=⌘+S}" IsVisible="{Binding #ThisControl.IsUnstagedChange}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Stage}"/>
<Run Text=" "/>
<Run Foreground="{DynamicResource Brush.FG2}" FontWeight="Normal" Text="{OnPlatform Ctrl+S, macOS=⌘+S}"/>
</TextBlock>
</Button>
<Button Classes="flat" Click="OnUnstageChunk" HotKey="{OnPlatform Ctrl+U, macOS=⌘+U}" IsVisible="{Binding #ThisControl.IsUnstagedChange, Converter={x:Static BoolConverters.Not}}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Unstage}"/>
<Run Text=" "/>
<Run Foreground="{DynamicResource Brush.FG2}" FontWeight="Normal" Text="{OnPlatform Ctrl+U, macOS=⌘+U}"/>
</TextBlock>
</Button>
<Button Classes="flat" Margin="8,0,0,0" HotKey="{OnPlatform Ctrl+D, macOS=⌘+D}" Click="OnDiscardChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}">
<TextBlock>
<Run Text="{DynamicResource Text.Hunk.Discard}"/>
<Run Text=" "/>
<Run Foreground="{DynamicResource Brush.FG2}" FontWeight="Normal" Text="{OnPlatform Ctrl+D, macOS=⌘+D}"/>
</TextBlock>
</Button>
</StackPanel>
</Grid>
</UserControl>

View file

@ -45,6 +45,18 @@ namespace SourceGit.Views
}
}
public record TextDiffViewRange
{
public int StartIdx { get; set; } = 0;
public int EndIdx { get; set; } = 0;
public TextDiffViewRange(int startIdx, int endIdx)
{
StartIdx = startIdx;
EndIdx = endIdx;
}
}
public class ThemedTextDiffPresenter : TextEditor
{
public class VerticalSeperatorMargin : AbstractMargin
@ -210,7 +222,6 @@ namespace SourceGit.Views
if (presenter == null)
return new Size(0, 0);
var maxLineNumber = presenter.GetMaxLineNumber();
var typeface = TextView.CreateTypeface();
var test = new FormattedText(
$"-",
@ -466,6 +477,15 @@ namespace SourceGit.Views
set => SetValue(SelectedChunkProperty, value);
}
public static readonly StyledProperty<TextDiffViewRange> DisplayRangeProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, TextDiffViewRange>(nameof(DisplayRange), new TextDiffViewRange(0, 0));
public TextDiffViewRange DisplayRange
{
get => GetValue(DisplayRangeProperty);
set => SetValue(DisplayRangeProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextEditor);
public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc)
@ -500,25 +520,11 @@ namespace SourceGit.Views
public void GotoPrevChange()
{
var view = TextArea.TextView;
var lines = GetLines();
var firstLineIdx = lines.Count;
foreach (var line in view.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var index = line.FirstDocumentLine.LineNumber - 1;
if (index >= lines.Count)
continue;
if (firstLineIdx > index)
firstLineIdx = index;
}
var firstLineIdx = DisplayRange.StartIdx;
if (firstLineIdx <= 1)
return;
var lines = GetLines();
var firstLineType = lines[firstLineIdx].Type;
var prevLineType = lines[firstLineIdx - 1].Type;
var isChangeFirstLine = firstLineType != Models.TextDiffLineType.Normal && firstLineType != Models.TextDiffLineType.Indicator;
@ -557,22 +563,8 @@ namespace SourceGit.Views
public void GotoNextChange()
{
var view = TextArea.TextView;
var lines = GetLines();
var lastLineIdx = -1;
foreach (var line in view.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var index = line.FirstDocumentLine.LineNumber - 1;
if (index >= lines.Count)
continue;
if (lastLineIdx < index)
lastLineIdx = index;
}
var lastLineIdx = DisplayRange.EndIdx;
if (lastLineIdx >= lines.Count - 1)
return;
@ -624,6 +616,7 @@ namespace SourceGit.Views
TextArea.TextView.PointerEntered += OnTextViewPointerChanged;
TextArea.TextView.PointerMoved += OnTextViewPointerChanged;
TextArea.TextView.PointerWheelChanged += OnTextViewPointerWheelChanged;
TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged;
UpdateTextMate();
}
@ -636,6 +629,7 @@ namespace SourceGit.Views
TextArea.TextView.PointerEntered -= OnTextViewPointerChanged;
TextArea.TextView.PointerMoved -= OnTextViewPointerChanged;
TextArea.TextView.PointerWheelChanged -= OnTextViewPointerWheelChanged;
TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged;
if (_textMate != null)
{
@ -743,6 +737,34 @@ namespace SourceGit.Views
}
}
private void OnTextViewVisualLinesChanged(object sender, EventArgs e)
{
if (!TextArea.TextView.VisualLinesValid)
{
SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(0, 0));
return;
}
var lines = GetLines();
var start = int.MaxValue;
var count = 0;
foreach (var line in TextArea.TextView.VisualLines)
{
if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted)
continue;
var index = line.FirstDocumentLine.LineNumber - 1;
if (index >= lines.Count)
continue;
count++;
if (start > index)
start = index;
}
SetCurrentValue(DisplayRangeProperty, new TextDiffViewRange(start, start + count));
}
protected void TrySetChunk(TextDiffViewChunk chunk)
{
var old = SelectedChunk;
@ -1050,14 +1072,10 @@ namespace SourceGit.Views
private void OnTextViewScrollGotFocus(object sender, GotFocusEventArgs e)
{
if (EnableChunkSelection && sender is ScrollViewer viewer)
{
var area = viewer.FindDescendantOfType<TextArea>();
if (!area.IsPointerOver)
if (EnableChunkSelection && !TextArea.IsPointerOver)
TrySetChunk(null);
}
}
}
public class SingleSideTextDiffPresenter : ThemedTextDiffPresenter
{
@ -1071,7 +1089,7 @@ namespace SourceGit.Views
public void ForceSyncScrollOffset()
{
if (DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer.Offset;
diff.SyncScrollOffset = _scrollViewer?.Offset ?? Vector.Zero;
}
public override List<Models.TextDiffLine> GetLines()
@ -1277,18 +1295,14 @@ namespace SourceGit.Views
private void OnTextViewScrollGotFocus(object sender, GotFocusEventArgs e)
{
if (EnableChunkSelection && sender is ScrollViewer viewer)
{
var area = viewer.FindDescendantOfType<TextArea>();
if (!area.IsPointerOver)
if (EnableChunkSelection && !TextArea.IsPointerOver)
TrySetChunk(null);
}
}
private void OnTextViewScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (TextArea.IsFocused && DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer.Offset;
diff.SyncScrollOffset = _scrollViewer?.Offset ?? Vector.Zero;
}
private void OnTextAreaPointerWheelChanged(object sender, PointerWheelEventArgs e)
@ -1300,6 +1314,124 @@ namespace SourceGit.Views
private ScrollViewer _scrollViewer = null;
}
public class TextDiffViewMinimap : Control
{
public static readonly StyledProperty<IBrush> AddedLineBrushProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, IBrush>(nameof(AddedLineBrush), new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)));
public IBrush AddedLineBrush
{
get => GetValue(AddedLineBrushProperty);
set => SetValue(AddedLineBrushProperty, value);
}
public static readonly StyledProperty<IBrush> DeletedLineBrushProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, IBrush>(nameof(DeletedLineBrush), new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)));
public IBrush DeletedLineBrush
{
get => GetValue(DeletedLineBrushProperty);
set => SetValue(DeletedLineBrushProperty, value);
}
public static readonly StyledProperty<TextDiffViewRange> DisplayRangeProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, TextDiffViewRange>(nameof(DisplayRange), new TextDiffViewRange(0, 0));
public TextDiffViewRange DisplayRange
{
get => GetValue(DisplayRangeProperty);
set => SetValue(DisplayRangeProperty, value);
}
public static readonly StyledProperty<Color> DisplayRangeColorProperty =
AvaloniaProperty.Register<TextDiffViewMinimap, Color>(nameof(DisplayRangeColor), Colors.RoyalBlue);
public Color DisplayRangeColor
{
get => GetValue(DisplayRangeColorProperty);
set => SetValue(DisplayRangeColorProperty, value);
}
static TextDiffViewMinimap()
{
AffectsRender<TextDiffViewMinimap>(
AddedLineBrushProperty,
DeletedLineBrushProperty,
DisplayRangeProperty,
DisplayRangeColorProperty);
}
public override void Render(DrawingContext context)
{
var total = 0;
if (DataContext is ViewModels.TwoSideTextDiff twoSideDiff)
{
var halfWidth = Bounds.Width * 0.5;
total = Math.Max(twoSideDiff.Old.Count, twoSideDiff.New.Count);
RenderSingleSide(context, twoSideDiff.Old, 0, halfWidth);
RenderSingleSide(context, twoSideDiff.New, halfWidth, halfWidth);
}
else if (DataContext is Models.TextDiff diff)
{
total = diff.Lines.Count;
RenderSingleSide(context, diff.Lines, 0, Bounds.Width);
}
var range = DisplayRange;
if (range.EndIdx == 0)
return;
var startY = range.StartIdx / (total * 1.0) * Bounds.Height;
var endY = range.EndIdx / (total * 1.0) * Bounds.Height;
var color = DisplayRangeColor;
var brush = new SolidColorBrush(color, 0.2);
var pen = new Pen(color.ToUInt32());
var rect = new Rect(0, startY, Bounds.Width, endY - startY);
context.DrawRectangle(brush, null, rect);
context.DrawLine(pen, rect.TopLeft, rect.TopRight);
context.DrawLine(pen, rect.BottomLeft, rect.BottomRight);
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
InvalidateVisual();
}
private void RenderSingleSide(DrawingContext context, List<Models.TextDiffLine> lines, double x, double width)
{
var total = lines.Count;
var lastLineType = Models.TextDiffLineType.Indicator;
var lastLineTypeStart = 0;
for (int i = 0; i < total; i++)
{
var line = lines[i];
if (line.Type != lastLineType)
{
RenderBlock(context, lastLineType, lastLineTypeStart, i - lastLineTypeStart, total, x, width);
lastLineType = line.Type;
lastLineTypeStart = i;
}
}
RenderBlock(context, lastLineType, lastLineTypeStart, total - lastLineTypeStart, total, x, width);
}
private void RenderBlock(DrawingContext context, Models.TextDiffLineType type, int start, int count, int total, double x, double width)
{
if (type == Models.TextDiffLineType.Added || type == Models.TextDiffLineType.Deleted)
{
var brush = type == Models.TextDiffLineType.Added ? AddedLineBrush : DeletedLineBrush;
var y = start / (total * 1.0) * Bounds.Height;
var h = Math.Max(0.5, count / (total * 1.0) * Bounds.Height);
context.DrawRectangle(brush, null, new Rect(x, y, width, h));
}
}
}
public partial class TextDiffView : UserControl
{
public static readonly StyledProperty<bool> UseSideBySideDiffProperty =
@ -1383,7 +1515,7 @@ namespace SourceGit.Views
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
RefreshContent(DataContext as Models.TextDiff, true);
RefreshContent(DataContext as Models.TextDiff);
}
protected override void OnPointerExited(PointerEventArgs e)