mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-25 21:07:20 -08:00
Compare commits
26 commits
c0eba5121e
...
34b2451d1d
Author | SHA1 | Date | |
---|---|---|---|
|
34b2451d1d | ||
|
e8c8ac55a1 | ||
|
f3c9690d47 | ||
|
88fb754415 | ||
|
f96f602456 | ||
|
3fb1c763f3 | ||
|
c611b62992 | ||
|
7a9c8d7444 | ||
|
8021cd8566 | ||
|
73687689ce | ||
|
b452e13453 | ||
|
814529a690 | ||
|
00804e453e | ||
|
b25f9bdb6c | ||
|
f45bed6f92 | ||
|
3be90b2ef6 | ||
|
f7ef61f1ce | ||
|
0da46cb90b | ||
|
8b3d129890 | ||
|
309db6e362 | ||
|
5b55e3530d | ||
|
d07a664166 | ||
|
ea1d966d27 | ||
|
f4618afee6 | ||
|
3b09ea45f5 | ||
|
13805f794a |
34 changed files with 964 additions and 201 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
8.38
|
||||
8.39
|
34
src/Commands/QueryCommitChildren.cs
Normal file
34
src/Commands/QueryCommitChildren.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SourceGit.ViewModels;
|
||||
|
||||
namespace SourceGit.Commands
|
||||
{
|
||||
public class QueryCommitChildren : Command
|
||||
{
|
||||
public QueryCommitChildren(string repo, string commit, string filters)
|
||||
{
|
||||
WorkingDirectory = repo;
|
||||
Context = repo;
|
||||
_commit = commit;
|
||||
if (string.IsNullOrEmpty(filters))
|
||||
filters = "--all";
|
||||
Args = $"rev-list -{Preference.Instance.MaxHistoryCommits} --parents {filters} ^{commit}";
|
||||
}
|
||||
|
||||
protected override void OnReadline(string line)
|
||||
{
|
||||
if (line.Contains(_commit))
|
||||
_lines.Add(line.Substring(0, 40));
|
||||
}
|
||||
|
||||
public IEnumerable<string> Result()
|
||||
{
|
||||
Exec();
|
||||
return _lines;
|
||||
}
|
||||
|
||||
private string _commit;
|
||||
private List<string> _lines = new List<string>();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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\""),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
410
src/Models/TemplateEngine.cs
Normal file
410
src/Models/TemplateEngine.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">INFORMATION</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTHOR</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CHANGED</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Children" xml:space="preserve">CHILDREN</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">COMMITTER</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.ContainsIn" xml:space="preserve">Check refs that contains this commit</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.ContainsIn.Title" xml:space="preserve">COMMIT IS CONTAINED BY</x:String>
|
||||
|
@ -451,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>
|
||||
|
@ -543,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>
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">INFORMATIONS</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTEUR</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CHANGÉ</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Children" xml:space="preserve">ENFANTS</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">COMMITTER</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.ContainsIn" xml:space="preserve">Vérifier les références contenant ce commit</x:String>
|
||||
<x:String x:Key="Text.CommitDetail.Info.ContainsIn.Title" xml:space="preserve">LE COMMIT EST CONTENU PAR</x:String>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -78,6 +78,12 @@ namespace SourceGit.ViewModels
|
|||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<string> Children
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new AvaloniaList<string>();
|
||||
|
||||
public string SearchChangeFilter
|
||||
{
|
||||
get => _searchChangeFilter;
|
||||
|
@ -515,6 +521,7 @@ namespace SourceGit.ViewModels
|
|||
VisibleChanges = null;
|
||||
SelectedChanges = null;
|
||||
ViewRevisionFileContent = null;
|
||||
Children.Clear();
|
||||
|
||||
if (_commit == null)
|
||||
return;
|
||||
|
@ -535,6 +542,18 @@ namespace SourceGit.ViewModels
|
|||
_cancelToken.Requested = true;
|
||||
|
||||
_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 };
|
||||
var children = cmdChildren.Result();
|
||||
if (!cmdChildren.Cancel.Requested)
|
||||
Dispatcher.UIThread.Post(() => Children.AddRange(children));
|
||||
});
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}" VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Base Information -->
|
||||
<Grid RowDefinitions="24,Auto,Auto,Auto" ColumnDefinitions="96,*">
|
||||
<Grid RowDefinitions="24,Auto,Auto,Auto,Auto" ColumnDefinitions="96,*">
|
||||
<!-- SHA -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.SHA}" />
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
|
||||
|
@ -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,56 @@
|
|||
</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}}"/>
|
||||
<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"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Classes="primary"
|
||||
Text="{Binding Converter={x:Static c:StringConverters.ToShortSHA}}"
|
||||
Foreground="DarkOrange"
|
||||
TextDecorations="Underline"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,16,0"
|
||||
PointerEntered="OnSHAPointerEntered"
|
||||
PointerPressed="OnSHAPressed">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="ToolTip">
|
||||
<Setter Property="MaxWidth" Value="600"/>
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
|
||||
<TextBlock.DataTemplates>
|
||||
<DataTemplate DataType="m:Commit">
|
||||
<StackPanel MinWidth="400" Orientation="Vertical">
|
||||
<Grid ColumnDefinitions="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"/>
|
||||
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding CommitterTimeStr}" Foreground="{DynamicResource Brush.FG2}" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="primary" Margin="0,8,0,0" Text="{Binding Subject}" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</TextBlock.DataTemplates>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- REFS -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Refs}" IsVisible="{Binding HasDecorators}"/>
|
||||
<Border Grid.Row="2" Grid.Column="1" Margin="12,0,0,0" Height="24" IsVisible="{Binding HasDecorators}">
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Refs}" IsVisible="{Binding HasDecorators}"/>
|
||||
<Border Grid.Row="3" Grid.Column="1" Margin="12,0,0,0" Height="24" IsVisible="{Binding HasDecorators}">
|
||||
<v:CommitRefsPresenter TagBackground="{DynamicResource Brush.DecoratorTag}"
|
||||
Foreground="{DynamicResource Brush.FG1}"
|
||||
FontFamily="{DynamicResource Fonts.Primary}"
|
||||
|
@ -155,8 +202,8 @@
|
|||
</Border>
|
||||
|
||||
<!-- Messages -->
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
|
||||
<v:CommitMessagePresenter Grid.Row="4" Grid.Column="1"
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
|
||||
<v:CommitMessagePresenter Grid.Row="5" Grid.Column="1"
|
||||
Margin="12,5,8,0"
|
||||
Classes="primary"
|
||||
Message="{Binding #ThisControl.Message}"
|
||||
|
|
|
@ -55,6 +55,15 @@ namespace SourceGit.Views
|
|||
set => SetValue(IssueTrackerRulesProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<AvaloniaList<string>> ChildrenProperty =
|
||||
AvaloniaProperty.Register<CommitBaseInfo, AvaloniaList<string>>(nameof(Children));
|
||||
|
||||
public AvaloniaList<string> Children
|
||||
{
|
||||
get => GetValue(ChildrenProperty);
|
||||
set => SetValue(ChildrenProperty, value);
|
||||
}
|
||||
|
||||
public CommitBaseInfo()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
SignInfo="{Binding SignInfo}"
|
||||
SupportsContainsIn="True"
|
||||
WebLinks="{Binding WebLinks}"
|
||||
Children="{Binding Children}"
|
||||
IssueTrackerRules="{Binding IssueTrackerRules}"/>
|
||||
|
||||
<!-- Line -->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}}"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue