Compare commits

...

19 commits

Author SHA1 Message Date
leo
dcf5093406
enhance: avoid that diff view refresh more than one times
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-04 18:21:07 +08:00
github-actions[bot]
a5aa2254f6 doc: Update translation status and missing keys 2024-11-04 08:49:32 +00:00
leo
6209326fe0
code_review: PR #652
* update localization for zh_CN and zh_TW
* change the icon for `Icons.Lines.All`
* reorder diff view toolbar buttons
* move private methods after protected

Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 16:49:15 +08:00
github-actions[bot]
8d4afafd2d doc: Update translation status and missing keys 2024-11-04 08:33:05 +00:00
Göran W
1a8acbf934
feature: diff - toggle show all lines (#615) (#652)
* Renamed 1 of 2 SyncScrollOffset props, for clarity

The property "SyncScrollOffset" in TextDiff is distinct from the one with the same name in TwoSideTextDiff. These two properties are used in separate (though slightly related) ways and are not really connected.

The one in TwoSideTextDiff is mainly used to keep the scroll-pos of the two SingleSideTextDiffPresenter views in sync (aligned), while the one in TextDiff is used only to preserve/reset the scroll-pos in the single CombinedTextDiffPresenter view when (re)loading Diff Content (so not really syncing anything).

To clarify this and to make the two properties more distinguishable, I renamed the one in TextDiff to simply "ScrollOffset".

* Added icon and string for "Show All Lines"

New StreamGeometry "Icons.Lines.All" using SVG path from "text_line_spacing_regular" at https://avaloniaui.github.io/icons.html.
New String "Text.Diff.VisualLines.All" for en_US locale (no translations yet).

* Implemented new TextDiff feature "Show All Lines" (toggle)

* Added new ToggleButton in DiffView toolbar, visible when IsTextDiff, disabling the buttons "Increase/Decrease Number of Visible Lines" when on.

* Added new Preference property "UseFullTextDiff".

* Added StyledProperty "UseFullTextDiffProperty" in TextDiffView, with a DataTemplate binding to the corresponding preference property.

* When changed, UseFullTextDiffProperty is handled identically as UseSideBySideDiffProperty (via new helper method RefreshContent(), for unification with OnDataContextChanged()).

* Added new method DiffContext.ToggleFullTextDiff() for changing the preference property and reloading the diff content.

* Implemented the new feature by overriding the "unified" (number of context lines) for Commands.Diff() with a very high number.

NOTE: The number used (~1 billion) is supposed to be the highest one working on Mac, according to this forum comment: https://stackoverflow.com/questions/28727424/for-git-diff-is-there-a-uinfinity-option-to-show-the-whole-file#comment135202820_28846576
2024-11-04 16:32:51 +08:00
leo
779b38be28
localization: change OpenAI to AI
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 16:04:19 +08:00
leo
ad01eb442d
localization: change OpenAI to AI
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 16:00:40 +08:00
leo
635396008d
fix: clicking Open in Browser context menu item of issue link does not work (#651)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 15:48:28 +08:00
github-actions[bot]
2f628b0f06 doc: Update translation status and missing keys 2024-11-04 07:35:07 +00:00
leo
fb9e342ee0
localization: add translations for zh_CN and zh_TW
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 15:34:50 +08:00
github-actions[bot]
25028efa4d doc: Update translation status and missing keys 2024-11-04 07:32:24 +00:00
leo
163e8cc0a4
feature: add context menu for issue link in commit details panel (#651)
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 15:31:55 +08:00
leo
64860950c7
localization: update en_US
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 12:03:47 +08:00
leo
310c786693
feature: ignore case when finding visual studio solution file
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 11:54:18 +08:00
leo
921b8599df
Merge branch 'master' into develop 2024-11-04 10:07:37 +08:00
leo
9452b796a5
Merge branch 'release/v8.37' 2024-11-04 10:07:12 +08:00
leo
5966b8ac08
version: Release 8.37
Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 10:06:51 +08:00
leo
6b348fbd1a
code_review!: PR #648
* rewrite `Models.ExternalTool` to use `_execArgsGenerator` instead of `OpenCmdArgs` and `ArgTransform`
* remove dependency of `System.Linq` due to AOT limitations
* since the `Visual Studio` is only available on Windows, use `TryAdd` directly.
* update `README.md`

BREAKING CHANGE: now the key in `external_editors.json` uses the same name with external tool.

Signed-off-by: leo <longshuang@msn.cn>
2024-11-04 10:02:20 +08:00
Dmitrij D. Czarkoff
1d0098703e
feature: add support for Visual Studio as external tool (#648)
* feature: support Visual Studio external tool on Windows
* feature: when opening in Visual Studio, try to locate solution file
2024-11-04 09:22:16 +08:00
36 changed files with 352 additions and 172 deletions

View file

@ -47,7 +47,7 @@
## Translation Status ## 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-96.05%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-97.08%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-87.72%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-90.79%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-95.63%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-96.65%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-87.34%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-90.39%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-99.56%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 ## How to Use
@ -119,14 +119,15 @@ For other AI service:
This app supports open repository in external tools listed in the table below. This app supports open repository in external tools listed in the table below.
| Tool | Windows | macOS | Linux | KEY IN `external_editors.json` | | Tool | Windows | macOS | Linux |
|-------------------------------|---------|-------|-------|--------------------------------| |-------------------------------|---------|-------|-------|
| Visual Studio Code | YES | YES | YES | VSCODE | | Visual Studio Code | YES | YES | YES |
| Visual Studio Code - Insiders | YES | YES | YES | VSCODE_INSIDERS | | Visual Studio Code - Insiders | YES | YES | YES |
| VSCodium | YES | YES | YES | VSCODIUM | | VSCodium | YES | YES | YES |
| JetBrains Fleet | YES | YES | YES | FLEET | | Fleet | YES | YES | YES |
| Sublime Text | YES | YES | YES | SUBLIME_TEXT | | Sublime Text | YES | YES | YES |
| Zed | NO | YES | YES | ZED | | Zed | NO | YES | YES |
| Visual Studio | YES | NO | NO |
> [!NOTE] > [!NOTE]
> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app. > This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app.
@ -134,7 +135,7 @@ This app supports open repository in external tools listed in the table below.
```json ```json
{ {
"tools": { "tools": {
"VSCODE": "D:\\VSCode\\Code.exe" "Visual Studio Code": "D:\\VSCode\\Code.exe"
} }
} }
``` ```

View file

@ -1,4 +1,4 @@
### de_DE.axaml: 96.05% ### de_DE.axaml: 95.63%
<details> <details>
@ -22,8 +22,11 @@
- Text.Configure.OpenAI - Text.Configure.OpenAI
- Text.Configure.OpenAI.Prefered - Text.Configure.OpenAI.Prefered
- Text.Configure.OpenAI.Prefered.Tip - Text.Configure.OpenAI.Prefered.Tip
- Text.Diff.VisualLines.All
- Text.ExecuteCustomAction - Text.ExecuteCustomAction
- Text.ExecuteCustomAction.Name - Text.ExecuteCustomAction.Name
- Text.IssueLinkCM.OpenInBrowser
- Text.IssueLinkCM.CopyLink
- Text.Preference.AI.AnalyzeDiffPrompt - Text.Preference.AI.AnalyzeDiffPrompt
- Text.Preference.AI.GenerateSubjectPrompt - Text.Preference.AI.GenerateSubjectPrompt
- Text.Preference.AI.Name - Text.Preference.AI.Name
@ -34,7 +37,7 @@
</details> </details>
### es_ES.axaml: 97.08% ### es_ES.axaml: 96.65%
<details> <details>
@ -54,8 +57,11 @@
- Text.Configure.OpenAI - Text.Configure.OpenAI
- Text.Configure.OpenAI.Prefered - Text.Configure.OpenAI.Prefered
- Text.Configure.OpenAI.Prefered.Tip - Text.Configure.OpenAI.Prefered.Tip
- Text.Diff.VisualLines.All
- Text.ExecuteCustomAction - Text.ExecuteCustomAction
- Text.ExecuteCustomAction.Name - Text.ExecuteCustomAction.Name
- Text.IssueLinkCM.OpenInBrowser
- Text.IssueLinkCM.CopyLink
- Text.Preference.AI.Name - Text.Preference.AI.Name
- Text.Repository.CustomActions - Text.Repository.CustomActions
- Text.Repository.CustomActions.Empty - Text.Repository.CustomActions.Empty
@ -63,7 +69,7 @@
</details> </details>
### fr_FR.axaml: 87.72% ### fr_FR.axaml: 87.34%
<details> <details>
@ -108,6 +114,7 @@
- Text.ConventionalCommit.ShortDescription - Text.ConventionalCommit.ShortDescription
- Text.ConventionalCommit.Type - Text.ConventionalCommit.Type
- Text.Diff.IgnoreWhitespace - Text.Diff.IgnoreWhitespace
- Text.Diff.VisualLines.All
- Text.Discard.IncludeIgnored - Text.Discard.IncludeIgnored
- Text.ExecuteCustomAction - Text.ExecuteCustomAction
- Text.ExecuteCustomAction.Name - Text.ExecuteCustomAction.Name
@ -119,6 +126,8 @@
- Text.Histories.Tips.Prefix - Text.Histories.Tips.Prefix
- Text.Hotkeys.Repo.CommitWithAutoStage - Text.Hotkeys.Repo.CommitWithAutoStage
- Text.Hotkeys.Repo.DiscardSelected - Text.Hotkeys.Repo.DiscardSelected
- Text.IssueLinkCM.OpenInBrowser
- Text.IssueLinkCM.CopyLink
- Text.MoveRepositoryNode - Text.MoveRepositoryNode
- Text.MoveRepositoryNode.Target - Text.MoveRepositoryNode.Target
- Text.Preference.AI - Text.Preference.AI
@ -156,7 +165,7 @@
</details> </details>
### pt_BR.axaml: 90.79% ### pt_BR.axaml: 90.39%
<details> <details>
@ -203,12 +212,15 @@
- Text.ConventionalCommit.ShortDescription - Text.ConventionalCommit.ShortDescription
- Text.ConventionalCommit.Type - Text.ConventionalCommit.Type
- Text.CopyAllText - Text.CopyAllText
- Text.Diff.VisualLines.All
- Text.Discard.IncludeIgnored - Text.Discard.IncludeIgnored
- Text.ExecuteCustomAction - Text.ExecuteCustomAction
- Text.ExecuteCustomAction.Name - Text.ExecuteCustomAction.Name
- Text.FileHistory.FileContent - Text.FileHistory.FileContent
- Text.FileHistory.FileChange - Text.FileHistory.FileChange
- Text.GitLFS.Locks.OnlyMine - Text.GitLFS.Locks.OnlyMine
- Text.IssueLinkCM.OpenInBrowser
- Text.IssueLinkCM.CopyLink
- Text.MoveRepositoryNode - Text.MoveRepositoryNode
- Text.MoveRepositoryNode.Target - Text.MoveRepositoryNode.Target
- Text.Preference.AI.Name - Text.Preference.AI.Name
@ -228,13 +240,15 @@
</details> </details>
### ru_RU.axaml: 100.00% ### ru_RU.axaml: 99.56%
<details> <details>
<summary>Missing Keys</summary> <summary>Missing Keys</summary>
- Text.Diff.VisualLines.All
- Text.IssueLinkCM.OpenInBrowser
- Text.IssueLinkCM.CopyLink
</details> </details>

View file

@ -1 +1 @@
8.36 8.37

View file

@ -8,6 +8,10 @@ namespace SourceGit.Commands
{ {
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR(); private static partial Regex REG_INDICATOR();
[GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")]
private static partial Regex REG_HASH_CHANGE();
private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_DEL = "-version https://git-lfs.github.com/spec/";
private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/"; private const string PREFIX_LFS_MODIFY = " version https://git-lfs.github.com/spec/";
@ -101,17 +105,31 @@ namespace SourceGit.Commands
if (_result.TextDiff.Lines.Count == 0) if (_result.TextDiff.Lines.Count == 0)
{ {
var match = REG_INDICATOR().Match(line); if (line.StartsWith("Binary", StringComparison.Ordinal))
if (!match.Success)
{ {
if (line.StartsWith("Binary", StringComparison.Ordinal)) _result.IsBinary = true;
_result.IsBinary = true;
return; return;
} }
_oldLine = int.Parse(match.Groups[1].Value); if (string.IsNullOrEmpty(_result.OldHash))
_newLine = int.Parse(match.Groups[2].Value); {
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0)); var match = REG_HASH_CHANGE().Match(line);
if (!match.Success)
return;
_result.OldHash = match.Groups[1].Value;
_result.NewHash = match.Groups[2].Value;
}
else
{
var match = REG_INDICATOR().Match(line);
if (!match.Success)
return;
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
}
} }
else else
{ {

View file

@ -30,7 +30,7 @@ namespace SourceGit.Models
public string Arguments public string Arguments
{ {
get => _arguments; get => _arguments;
set => SetProperty(ref _arguments, value); set => SetProperty(ref _arguments, value);
} }

View file

@ -63,7 +63,7 @@ namespace SourceGit.Models
{ {
public string File { get; set; } = string.Empty; public string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>(); public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public Vector SyncScrollOffset { get; set; } = Vector.Zero; public Vector ScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0; public int MaxLineNumber = 0;
public string Repo { get; set; } = null; public string Repo { get; set; } = null;
@ -674,6 +674,8 @@ namespace SourceGit.Models
{ {
public bool IsBinary { get; set; } = false; public bool IsBinary { get; set; } = false;
public bool IsLFS { get; set; } = false; public bool IsLFS { get; set; } = false;
public string OldHash { get; set; } = string.Empty;
public string NewHash { get; set; } = string.Empty;
public string OldMode { get; set; } = string.Empty; public string OldMode { get; set; } = string.Empty;
public string NewMode { get; set; } = string.Empty; public string NewMode { get; set; } = string.Empty;
public TextDiff TextDiff { get; set; } = null; public TextDiff TextDiff { get; set; } = null;

View file

@ -13,15 +13,13 @@ namespace SourceGit.Models
public class ExternalTool public class ExternalTool
{ {
public string Name { get; private set; } public string Name { get; private set; }
public string Executable { get; private set; }
public string OpenCmdArgs { get; private set; }
public Bitmap IconImage { get; private set; } = null; public Bitmap IconImage { get; private set; } = null;
public ExternalTool(string name, string icon, string executable, string openCmdArgs) public ExternalTool(string name, string icon, string execFile, Func<string, string> execArgsGenerator = null)
{ {
Name = name; Name = name;
Executable = executable; _execFile = execFile;
OpenCmdArgs = openCmdArgs; _execArgsGenerator = execArgsGenerator ?? (repo => $"\"{repo}\"");
try try
{ {
@ -40,11 +38,14 @@ namespace SourceGit.Models
Process.Start(new ProcessStartInfo() Process.Start(new ProcessStartInfo()
{ {
WorkingDirectory = repo, WorkingDirectory = repo,
FileName = Executable, FileName = _execFile,
Arguments = string.Format(OpenCmdArgs, repo), Arguments = _execArgsGenerator.Invoke(repo),
UseShellExecute = false, UseShellExecute = false,
}); });
} }
private string _execFile = string.Empty;
private Func<string, string> _execArgsGenerator = null;
} }
public class JetBrainsState public class JetBrainsState
@ -110,48 +111,48 @@ namespace SourceGit.Models
_customPaths = new ExternalToolPaths(); _customPaths = new ExternalToolPaths();
} }
public void TryAdd(string name, string icon, string args, string key, Func<string> finder) public void TryAdd(string name, string icon, Func<string> finder, Func<string, string> execArgsGenerator = null)
{ {
if (_customPaths.Tools.TryGetValue(key, out var customPath) && File.Exists(customPath)) if (_customPaths.Tools.TryGetValue(name, out var customPath) && File.Exists(customPath))
{ {
Founded.Add(new ExternalTool(name, icon, customPath, args)); Founded.Add(new ExternalTool(name, icon, customPath, execArgsGenerator));
} }
else else
{ {
var path = finder(); var path = finder();
if (!string.IsNullOrEmpty(path) && File.Exists(path)) if (!string.IsNullOrEmpty(path) && File.Exists(path))
Founded.Add(new ExternalTool(name, icon, path, args)); Founded.Add(new ExternalTool(name, icon, path, execArgsGenerator));
} }
} }
public void VSCode(Func<string> platformFinder) public void VSCode(Func<string> platformFinder)
{ {
TryAdd("Visual Studio Code", "vscode", "\"{0}\"", "VSCODE", platformFinder); TryAdd("Visual Studio Code", "vscode", platformFinder);
} }
public void VSCodeInsiders(Func<string> platformFinder) public void VSCodeInsiders(Func<string> platformFinder)
{ {
TryAdd("Visual Studio Code - Insiders", "vscode_insiders", "\"{0}\"", "VSCODE_INSIDERS", platformFinder); TryAdd("Visual Studio Code - Insiders", "vscode_insiders", platformFinder);
} }
public void VSCodium(Func<string> platformFinder) public void VSCodium(Func<string> platformFinder)
{ {
TryAdd("VSCodium", "codium", "\"{0}\"", "VSCODIUM", platformFinder); TryAdd("VSCodium", "codium", platformFinder);
} }
public void Fleet(Func<string> platformFinder) public void Fleet(Func<string> platformFinder)
{ {
TryAdd("Fleet", "fleet", "\"{0}\"", "FLEET", platformFinder); TryAdd("Fleet", "fleet", platformFinder);
} }
public void SublimeText(Func<string> platformFinder) public void SublimeText(Func<string> platformFinder)
{ {
TryAdd("Sublime Text", "sublime_text", "\"{0}\"", "SUBLIME_TEXT", platformFinder); TryAdd("Sublime Text", "sublime_text", platformFinder);
} }
public void Zed(Func<string> platformFinder) public void Zed(Func<string> platformFinder)
{ {
TryAdd("Zed", "zed", "\"{0}\"", "ZED", platformFinder); TryAdd("Zed", "zed", platformFinder);
} }
public void FindJetBrainsFromToolbox(Func<string> platformFinder) public void FindJetBrainsFromToolbox(Func<string> platformFinder)
@ -170,8 +171,7 @@ namespace SourceGit.Models
Founded.Add(new ExternalTool( Founded.Add(new ExternalTool(
$"{tool.DisplayName} {tool.DisplayVersion}", $"{tool.DisplayName} {tool.DisplayVersion}",
supported_icons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", supported_icons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB",
Path.Combine(tool.InstallLocation, tool.LaunchCommand), Path.Combine(tool.InstallLocation, tool.LaunchCommand)));
"\"{0}\""));
} }
} }
} }

View file

@ -134,6 +134,7 @@ namespace SourceGit.Native
finder.Fleet(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\Programs\\Fleet\\Fleet.exe"); finder.Fleet(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\Programs\\Fleet\\Fleet.exe");
finder.FindJetBrainsFromToolbox(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\JetBrains\\Toolbox"); finder.FindJetBrainsFromToolbox(() => $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\\JetBrains\\Toolbox");
finder.SublimeText(FindSublimeText); finder.SublimeText(FindSublimeText);
finder.TryAdd("Visual Studio", "vs", FindVisualStudio, GenerateCommandlineArgsForVisualStudio);
return finder.Founded; return finder.Founded;
} }
@ -313,6 +314,27 @@ namespace SourceGit.Native
return string.Empty; return string.Empty;
} }
private string FindVisualStudio()
{
var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey(
Microsoft.Win32.RegistryHive.LocalMachine,
Microsoft.Win32.RegistryView.Registry64);
// Get default class for VisualStudio.Launcher.sln - the handler for *.sln files
if (localMachine.OpenSubKey(@"SOFTWARE\Classes\VisualStudio.Launcher.sln\CLSID") is Microsoft.Win32.RegistryKey launcher)
{
// Get actual path to the executable
if (launcher.GetValue(string.Empty) is string CLSID &&
localMachine.OpenSubKey(@$"SOFTWARE\Classes\CLSID\{CLSID}\LocalServer32") is Microsoft.Win32.RegistryKey devenv &&
devenv.GetValue(string.Empty) is string localServer32)
{
return localServer32!.Trim('\"');
}
}
return string.Empty;
}
#endregion #endregion
private void OpenFolderAndSelectFile(string folderPath) private void OpenFolderAndSelectFile(string folderPath)
@ -328,5 +350,34 @@ namespace SourceGit.Native
ILFree(pidl); ILFree(pidl);
} }
} }
private string GenerateCommandlineArgsForVisualStudio(string repo)
{
var sln = FindVSSolutionFile(new DirectoryInfo(repo), 4);
return string.IsNullOrEmpty(sln) ? $"\"{repo}\"" : $"\"{sln}\"";
}
private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth)
{
var files = dir.GetFiles();
foreach (var f in files)
{
if (f.Name.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
return f.FullName;
}
if (leftDepth <= 0)
return null;
var subDirs = dir.GetDirectories();
foreach (var subDir in subDirs)
{
var first = FindVSSolutionFile(subDir, leftDepth - 1);
if (!string.IsNullOrEmpty(first))
return first;
}
return null;
}
} }
} }

View file

@ -65,6 +65,7 @@
<StreamGeometry x:Key="Icons.LayoutHorizontal">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z</StreamGeometry> <StreamGeometry x:Key="Icons.LayoutHorizontal">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutVertical">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z</StreamGeometry> <StreamGeometry x:Key="Icons.LayoutVertical">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z</StreamGeometry>
<StreamGeometry x:Key="Icons.LFS">M40 9 15 23 15 31 9 28 9 20 34 5 24 0 0 14 0 34 25 48 25 28 49 14zM26 29 26 48 49 34 49 15z</StreamGeometry> <StreamGeometry x:Key="Icons.LFS">M40 9 15 23 15 31 9 28 9 20 34 5 24 0 0 14 0 34 25 48 25 28 49 14zM26 29 26 48 49 34 49 15z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.All">M416 192m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM416 448m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM416 704m32 0 448 0q32 0 32 32l0 0q0 32-32 32l-448 0q-32 0-32-32l0 0q0-32 32-32ZM96 320l128-192 128 192h-256zM96 640l128 192 128-192h-256zM190 320h64v320H190z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.Incr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z</StreamGeometry> <StreamGeometry x:Key="Icons.Lines.Incr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Lines.Decr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z</StreamGeometry> <StreamGeometry x:Key="Icons.Lines.Decr">M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Link">M 968 418 l -95 94 c -59 59 -146 71 -218 37 L 874 331 a 64 64 0 0 0 0 -90 L 783 150 a 64 64 0 0 0 -90 0 L 475 368 c -34 -71 -22 -159 37 -218 l 94 -94 c 75 -75 196 -75 271 0 l 90 90 c 75 75 75 196 0 271 z M 332 693 a 64 64 0 0 1 0 -90 l 271 -271 c 25 -25 65 -25 90 0 s 25 65 0 90 L 422 693 a 64 64 0 0 1 -90 0 z M 151 783 l 90 90 a 64 64 0 0 0 90 0 l 218 -218 c 34 71 22 159 -37 218 l -86 94 a 192 192 0 0 1 -271 0 l -98 -98 a 192 192 0 0 1 0 -271 l 94 -86 c 59 -59 146 -71 218 -37 L 151 693 a 64 64 0 0 0 0 90 z</StreamGeometry> <StreamGeometry x:Key="Icons.Link">M 968 418 l -95 94 c -59 59 -146 71 -218 37 L 874 331 a 64 64 0 0 0 0 -90 L 783 150 a 64 64 0 0 0 -90 0 L 475 368 c -34 -71 -22 -159 37 -218 l 94 -94 c 75 -75 196 -75 271 0 l 90 90 c 75 75 75 196 0 271 z M 332 693 a 64 64 0 0 1 0 -90 l 271 -271 c 25 -25 65 -25 90 0 s 25 65 0 90 L 422 693 a 64 64 0 0 1 -90 0 z M 151 783 l 90 90 a 64 64 0 0 0 90 0 l 218 -218 c 34 71 22 159 -37 218 l -86 94 a 192 192 0 0 1 -271 0 l -98 -98 a 192 192 0 0 1 0 -271 l 94 -86 c 59 -59 146 -71 218 -37 L 151 693 a 64 64 0 0 0 0 90 z</StreamGeometry>

View file

@ -18,8 +18,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String> <x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">Optional. Default is the destination folder name.</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String> <x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">Track Branch:</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String> <x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">Tracking remote branch</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI Assistant</x:String> <x:String x:Key="Text.AIAssistant" xml:space="preserve">AI Assistant</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use OpenAI to generate commit message</x:String> <x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">Use AI to generate commit message</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String> <x:String x:Key="Text.Apply" xml:space="preserve">Patch</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String> <x:String x:Key="Text.Apply.Error" xml:space="preserve">Error</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">Raise errors and refuses to apply the patch</x:String>
@ -166,7 +166,7 @@
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">Rule Name:</x:String> <x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">Rule Name:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">Result URL:</x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">Result URL:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">Please use $1, $2 to access regex groups values.</x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">Please use $1, $2 to access regex groups values.</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">OPEN AI</x:String> <x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">Prefered Service:</x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">Prefered Service:</x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">If the 'Prefered Service' is set, SourceGit will only use it in this repository. Otherwise, if there is more than one service available, a context menu to choose one of them will be shown.</x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">If the 'Prefered Service' is set, SourceGit will only use it in this repository. Otherwise, if there is more than one service available, a context menu to choose one of them will be shown.</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP Proxy</x:String> <x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP Proxy</x:String>
@ -246,6 +246,7 @@
<x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">Syntax Highlighting</x:String> <x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">Syntax Highlighting</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">Line Word Wrap</x:String> <x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">Line Word Wrap</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">Open in Merge Tool</x:String> <x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">Open in Merge Tool</x:String>
<x:String x:Key="Text.Diff.VisualLines.All" xml:space="preserve">Show All Lines</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">Decrease Number of Visible Lines</x:String> <x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">Decrease Number of Visible Lines</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">Increase Number of Visible Lines</x:String> <x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">Increase Number of Visible Lines</x:String>
<x:String x:Key="Text.Diff.Welcome" xml:space="preserve">SELECT FILE TO VIEW CHANGES</x:String> <x:String x:Key="Text.Diff.Welcome" xml:space="preserve">SELECT FILE TO VIEW CHANGES</x:String>
@ -274,7 +275,7 @@
<x:String x:Key="Text.FileCM.DiscardMulti" xml:space="preserve">Discard {0} files...</x:String> <x:String x:Key="Text.FileCM.DiscardMulti" xml:space="preserve">Discard {0} files...</x:String>
<x:String x:Key="Text.FileCM.DiscardSelectedLines" xml:space="preserve">Discard Changes in Selected Line(s)</x:String> <x:String x:Key="Text.FileCM.DiscardSelectedLines" xml:space="preserve">Discard Changes in Selected Line(s)</x:String>
<x:String x:Key="Text.FileCM.OpenWithExternalMerger" xml:space="preserve">Open External Merge Tool</x:String> <x:String x:Key="Text.FileCM.OpenWithExternalMerger" xml:space="preserve">Open External Merge Tool</x:String>
<x:String x:Key="Text.FileCM.SaveAsPatch" xml:space="preserve">Save As Patch...</x:String> <x:String x:Key="Text.FileCM.SaveAsPatch" xml:space="preserve">Save as Patch...</x:String>
<x:String x:Key="Text.FileCM.Stage" xml:space="preserve">Stage</x:String> <x:String x:Key="Text.FileCM.Stage" xml:space="preserve">Stage</x:String>
<x:String x:Key="Text.FileCM.StageMulti" xml:space="preserve">Stage {0} files</x:String> <x:String x:Key="Text.FileCM.StageMulti" xml:space="preserve">Stage {0} files</x:String>
<x:String x:Key="Text.FileCM.StageSelectedLines" xml:space="preserve">Stage Changes in Selected Line(s)</x:String> <x:String x:Key="Text.FileCM.StageSelectedLines" xml:space="preserve">Stage Changes in Selected Line(s)</x:String>
@ -387,6 +388,8 @@
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">Interactive Rebase</x:String> <x:String x:Key="Text.InteractiveRebase" xml:space="preserve">Interactive Rebase</x:String>
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">Target Branch:</x:String> <x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">Target Branch:</x:String>
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">On:</x:String> <x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">On:</x:String>
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">Open in Browser</x:String>
<x:String x:Key="Text.IssueLinkCM.CopyLink" xml:space="preserve">Copy Link</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String> <x:String x:Key="Text.Launcher.Error" xml:space="preserve">ERROR</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String> <x:String x:Key="Text.Launcher.Info" xml:space="preserve">NOTICE</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String> <x:String x:Key="Text.Merge" xml:space="preserve">Merge Branch</x:String>
@ -398,7 +401,7 @@
<x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String> <x:String x:Key="Text.Name" xml:space="preserve">Name:</x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">Git has NOT been configured. Please to go [Preference] and configure it first.</x:String> <x:String x:Key="Text.NotConfigured" xml:space="preserve">Git has NOT been configured. Please to go [Preference] and configure it first.</x:String>
<x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">Open App Data Dir</x:String> <x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">Open App Data Dir</x:String>
<x:String x:Key="Text.OpenWith" xml:space="preserve">Open With...</x:String> <x:String x:Key="Text.OpenWith" xml:space="preserve">Open with...</x:String>
<x:String x:Key="Text.Optional" xml:space="preserve">Optional.</x:String> <x:String x:Key="Text.Optional" xml:space="preserve">Optional.</x:String>
<x:String x:Key="Text.PageTabBar.New" xml:space="preserve">Create New Page</x:String> <x:String x:Key="Text.PageTabBar.New" xml:space="preserve">Create New Page</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">Bookmark</x:String> <x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">Bookmark</x:String>
@ -418,7 +421,7 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">Last year</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">Last year</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String> <x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String> <x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String>
@ -530,15 +533,15 @@
<x:String x:Key="Text.Repository.CustomActions" xml:space="preserve">Custom Actions</x:String> <x:String x:Key="Text.Repository.CustomActions" xml:space="preserve">Custom Actions</x:String>
<x:String x:Key="Text.Repository.CustomActions.Empty" xml:space="preserve">No Custom Actions</x:String> <x:String x:Key="Text.Repository.CustomActions.Empty" xml:space="preserve">No Custom Actions</x:String>
<x:String x:Key="Text.Repository.EnableReflog" xml:space="preserve">Enable '--reflog' Option</x:String> <x:String x:Key="Text.Repository.EnableReflog" xml:space="preserve">Enable '--reflog' Option</x:String>
<x:String x:Key="Text.Repository.Explore" xml:space="preserve">Open In File Browser</x:String> <x:String x:Key="Text.Repository.Explore" xml:space="preserve">Open in File Browser</x:String>
<x:String x:Key="Text.Repository.Filter" xml:space="preserve">Search Branches/Tags/Submodules</x:String> <x:String x:Key="Text.Repository.Filter" xml:space="preserve">Search Branches/Tags/Submodules</x:String>
<x:String x:Key="Text.Repository.FilterCommitPrefix" xml:space="preserve">FILTERED BY:</x:String> <x:String x:Key="Text.Repository.FilterCommitPrefix" xml:space="preserve">FILTERED BY:</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">LOCAL BRANCHES</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.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> <x:String x:Key="Text.Repository.FirstParentFilterToggle" xml:space="preserve">Enable '--first-parent' Option</x:String>
<x:String x:Key="Text.Repository.NewBranch" xml:space="preserve">Create Branch</x:String> <x:String x:Key="Text.Repository.NewBranch" xml:space="preserve">Create Branch</x:String>
<x:String x:Key="Text.Repository.OpenIn" xml:space="preserve">Open In {0}</x:String> <x:String x:Key="Text.Repository.OpenIn" xml:space="preserve">Open in {0}</x:String>
<x:String x:Key="Text.Repository.OpenWithExternalTools" xml:space="preserve">Open In External Tools</x:String> <x:String x:Key="Text.Repository.OpenWithExternalTools" xml:space="preserve">Open in External Tools</x:String>
<x:String x:Key="Text.Repository.Refresh" xml:space="preserve">Refresh</x:String> <x:String x:Key="Text.Repository.Refresh" xml:space="preserve">Refresh</x:String>
<x:String x:Key="Text.Repository.Remotes" xml:space="preserve">REMOTES</x:String> <x:String x:Key="Text.Repository.Remotes" xml:space="preserve">REMOTES</x:String>
<x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">ADD REMOTE</x:String> <x:String x:Key="Text.Repository.Remotes.Add" xml:space="preserve">ADD REMOTE</x:String>
@ -556,7 +559,7 @@
<x:String x:Key="Text.Repository.Submodules.Update" xml:space="preserve">UPDATE SUBMODULE</x:String> <x:String x:Key="Text.Repository.Submodules.Update" xml:space="preserve">UPDATE SUBMODULE</x:String>
<x:String x:Key="Text.Repository.Tags" xml:space="preserve">TAGS</x:String> <x:String x:Key="Text.Repository.Tags" xml:space="preserve">TAGS</x:String>
<x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">NEW TAG</x:String> <x:String x:Key="Text.Repository.Tags.Add" xml:space="preserve">NEW TAG</x:String>
<x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Open In Terminal</x:String> <x:String x:Key="Text.Repository.Terminal" xml:space="preserve">Open in Terminal</x:String>
<x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREES</x:String> <x:String x:Key="Text.Repository.Worktrees" xml:space="preserve">WORKTREES</x:String>
<x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">ADD WORKTREE</x:String> <x:String x:Key="Text.Repository.Worktrees.Add" xml:space="preserve">ADD WORKTREE</x:String>
<x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">PRUNE</x:String> <x:String x:Key="Text.Repository.Worktrees.Prune" xml:space="preserve">PRUNE</x:String>

View file

@ -21,8 +21,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String> <x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">选填。默认使用目标文件夹名称。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String> <x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">跟踪分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String> <x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">设置上游跟踪分支</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI助手</x:String> <x:String x:Key="Text.AIAssistant" xml:space="preserve">AI助手</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用OpenAI助手生成提交信息</x:String> <x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用AI助手生成提交信息</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String> <x:String x:Key="Text.Apply" xml:space="preserve">应用补丁(apply)</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String> <x:String x:Key="Text.Apply.Error" xml:space="preserve">错误</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">输出错误,并终止应用补丁</x:String>
@ -169,9 +169,9 @@
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">规则名 </x:String> <x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">规则名 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">为ISSUE生成的URL链接 </x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">为ISSUE生成的URL链接 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在URL中使用$1$2等变量填入正则表达式匹配的内容</x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在URL中使用$1$2等变量填入正则表达式匹配的内容</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">OPEN AI</x:String> <x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">启用特定服务 </x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">启用特定服务 </x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">当【启用特定服务】被设置时SourceGit将在本仓库中仅使用该服务。否则将弹出可用的OpenAI服务列表供用户选择。</x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">当【启用特定服务】被设置时SourceGit将在本仓库中仅使用该服务。否则将弹出可用的AI服务列表供用户选择。</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP代理</x:String> <x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP网络代理</x:String> <x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP网络代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">用户名</x:String> <x:String x:Key="Text.Configure.User" xml:space="preserve">用户名</x:String>
@ -249,6 +249,7 @@
<x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">语法高亮</x:String> <x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">语法高亮</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自动换行</x:String> <x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自动换行</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合并工具查看</x:String> <x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合并工具查看</x:String>
<x:String x:Key="Text.Diff.VisualLines.All" xml:space="preserve">显示完整文件</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">减少可见的行数</x:String> <x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">减少可见的行数</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可见的行数</x:String> <x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可见的行数</x:String>
<x:String x:Key="Text.Diff.Welcome" xml:space="preserve">请选择需要对比的文件</x:String> <x:String x:Key="Text.Diff.Welcome" xml:space="preserve">请选择需要对比的文件</x:String>
@ -390,6 +391,8 @@
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">交互式变基</x:String> <x:String x:Key="Text.InteractiveRebase" xml:space="preserve">交互式变基</x:String>
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目标分支 </x:String> <x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交 </x:String> <x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交 </x:String>
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">在浏览器中访问</x:String>
<x:String x:Key="Text.IssueLinkCM.CopyLink" xml:space="preserve">复制链接地址</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String> <x:String x:Key="Text.Launcher.Error" xml:space="preserve">出错了</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String> <x:String x:Key="Text.Launcher.Info" xml:space="preserve">系统提示</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">合并分支</x:String> <x:String x:Key="Text.Merge" xml:space="preserve">合并分支</x:String>
@ -421,7 +424,7 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0}年前</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0}年前</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好设置</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">偏好设置</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String> <x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API密钥</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API密钥</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String> <x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String>

View file

@ -21,8 +21,8 @@
<x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">選填。預設使用目標資料夾名稱。</x:String> <x:String x:Key="Text.AddWorktree.Name.Placeholder" xml:space="preserve">選填。預設使用目標資料夾名稱。</x:String>
<x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String> <x:String x:Key="Text.AddWorktree.Tracking" xml:space="preserve">追蹤分支</x:String>
<x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String> <x:String x:Key="Text.AddWorktree.Tracking.Toggle" xml:space="preserve">設定遠端追蹤分支</x:String>
<x:String x:Key="Text.AIAssistant" xml:space="preserve">OpenAI 助理</x:String> <x:String x:Key="Text.AIAssistant" xml:space="preserve">AI 助理</x:String>
<x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 OpenAI 產生提交訊息</x:String> <x:String x:Key="Text.AIAssistant.Tip" xml:space="preserve">使用 AI 產生提交訊息</x:String>
<x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String> <x:String x:Key="Text.Apply" xml:space="preserve">套用修補檔 (apply patch)</x:String>
<x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String> <x:String x:Key="Text.Apply.Error" xml:space="preserve">錯誤</x:String>
<x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String> <x:String x:Key="Text.Apply.Error.Desc" xml:space="preserve">輸出錯誤,並中止套用修補檔</x:String>
@ -169,9 +169,9 @@
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">規則名稱:</x:String> <x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">規則名稱:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">為 Issue 產生的網址連結:</x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">為 Issue 產生的網址連結:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在網址中使用 $1、$2 等變數填入正規表達式相符的內容</x:String> <x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在網址中使用 $1、$2 等變數填入正規表達式相符的內容</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">OpenAI</x:String> <x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">偏好服務:</x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered" xml:space="preserve">偏好服務:</x:String>
<x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">設定 [偏好服務] 後SourceGit 將於此存放庫中使用該服務,否則會顯示 OpenAI 服務列表供使用者選擇。</x:String> <x:String x:Key="Text.Configure.OpenAI.Prefered.Tip" xml:space="preserve">設定 [偏好服務] 後SourceGit 將於此存放庫中使用該服務,否則會顯示 AI 服務列表供使用者選擇。</x:String>
<x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP 代理</x:String> <x:String x:Key="Text.Configure.Proxy" xml:space="preserve">HTTP 代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP 網路代理</x:String> <x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP 網路代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">使用者名稱</x:String> <x:String x:Key="Text.Configure.User" xml:space="preserve">使用者名稱</x:String>
@ -249,6 +249,7 @@
<x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">語法上色</x:String> <x:String x:Key="Text.Diff.SyntaxHighlight" xml:space="preserve">語法上色</x:String>
<x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自動換行</x:String> <x:String x:Key="Text.Diff.ToggleWordWrap" xml:space="preserve">自動換行</x:String>
<x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合併工具檢視</x:String> <x:String x:Key="Text.Diff.UseMerger" xml:space="preserve">使用外部合併工具檢視</x:String>
<x:String x:Key="Text.Diff.VisualLines.All" xml:space="preserve">顯示檔案的全部內容</x:String>
<x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">減少可見的行數</x:String> <x:String x:Key="Text.Diff.VisualLines.Decr" xml:space="preserve">減少可見的行數</x:String>
<x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可見的行數</x:String> <x:String x:Key="Text.Diff.VisualLines.Incr" xml:space="preserve">增加可見的行數</x:String>
<x:String x:Key="Text.Diff.Welcome" xml:space="preserve">請選擇需要對比的檔案</x:String> <x:String x:Key="Text.Diff.Welcome" xml:space="preserve">請選擇需要對比的檔案</x:String>
@ -390,6 +391,8 @@
<x:String x:Key="Text.InteractiveRebase" xml:space="preserve">互動式重定基底</x:String> <x:String x:Key="Text.InteractiveRebase" xml:space="preserve">互動式重定基底</x:String>
<x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目標分支:</x:String> <x:String x:Key="Text.InteractiveRebase.Target" xml:space="preserve">目標分支:</x:String>
<x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交:</x:String> <x:String x:Key="Text.InteractiveRebase.On" xml:space="preserve">起始提交:</x:String>
<x:String x:Key="Text.IssueLinkCM.OpenInBrowser" xml:space="preserve">在瀏覽器中存取網址</x:String>
<x:String x:Key="Text.IssueLinkCM.CopyLink" xml:space="preserve">複製網址</x:String>
<x:String x:Key="Text.Launcher.Error" xml:space="preserve">發生錯誤</x:String> <x:String x:Key="Text.Launcher.Error" xml:space="preserve">發生錯誤</x:String>
<x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String> <x:String x:Key="Text.Launcher.Info" xml:space="preserve">系統提示</x:String>
<x:String x:Key="Text.Merge" xml:space="preserve">合併分支</x:String> <x:String x:Key="Text.Merge" xml:space="preserve">合併分支</x:String>
@ -421,7 +424,7 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} 年前</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} 年前</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好設定</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">偏好設定</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OpenAI</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">AI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">伺服器</x:String> <x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">伺服器</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API 金鑰</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API 金鑰</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String> <x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String>

View file

@ -33,12 +33,6 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _fileModeChange, value); private set => SetProperty(ref _fileModeChange, value);
} }
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public bool IsTextDiff public bool IsTextDiff
{ {
get => _isTextDiff; get => _isTextDiff;
@ -68,6 +62,7 @@ namespace SourceGit.ViewModels
_content = previous._content; _content = previous._content;
_unifiedLines = previous._unifiedLines; _unifiedLines = previous._unifiedLines;
_ignoreWhitespace = previous._ignoreWhitespace; _ignoreWhitespace = previous._ignoreWhitespace;
_info = previous._info;
} }
if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null")
@ -78,6 +73,12 @@ namespace SourceGit.ViewModels
LoadDiffContent(); LoadDiffContent();
} }
public void ToggleFullTextDiff()
{
Preference.Instance.UseFullTextDiff = !Preference.Instance.UseFullTextDiff;
LoadDiffContent();
}
public void IncrUnified() public void IncrUnified()
{ {
UnifiedLines = _unifiedLines + 1; UnifiedLines = _unifiedLines + 1;
@ -103,15 +104,23 @@ namespace SourceGit.ViewModels
{ {
Content = null; Content = null;
IsTextDiff = false; IsTextDiff = false;
IsLoading = false;
return; return;
} }
Task.Run(() => Task.Run(() =>
{ {
var latest = new Commands.Diff(_repo, _option, _unifiedLines, _ignoreWhitespace).Result(); // NOTE: Here we override the UnifiedLines value (if UseFullTextDiff is on).
var rs = null as object; // There is no way to tell a git-diff to use "ALL lines of context",
// so instead we set a very high number for the "lines of context" parameter.
var numLines = Preference.Instance.UseFullTextDiff ? 999999999 : _unifiedLines;
var latest = new Commands.Diff(_repo, _option, numLines, _ignoreWhitespace).Result();
var info = new Info(_option, numLines, _ignoreWhitespace, latest);
if (_info != null && info.IsSame(_info))
return;
_info = info;
var rs = null as object;
if (latest.TextDiff != null) if (latest.TextDiff != null)
{ {
var count = latest.TextDiff.Lines.Count; var count = latest.TextDiff.Lines.Count;
@ -203,12 +212,11 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (_content is Models.TextDiff old && rs is Models.TextDiff cur && old.File == cur.File) if (_content is Models.TextDiff old && rs is Models.TextDiff cur && old.File == cur.File)
cur.SyncScrollOffset = old.SyncScrollOffset; cur.ScrollOffset = old.ScrollOffset;
FileModeChange = latest.FileModeChange; FileModeChange = latest.FileModeChange;
Content = rs; Content = rs;
IsTextDiff = rs is Models.TextDiff; IsTextDiff = rs is Models.TextDiff;
IsLoading = false;
}); });
}); });
} }
@ -241,14 +249,41 @@ namespace SourceGit.ViewModels
".ico", ".bmp", ".jpg", ".png", ".jpeg", ".webp" ".ico", ".bmp", ".jpg", ".png", ".jpeg", ".webp"
}; };
private class Info
{
public string Argument { get; set; }
public int UnifiedLines { get; set; }
public bool IgnoreWhitespace { get; set; }
public string OldHash { get; set; }
public string NewHash { get; set; }
public Info(Models.DiffOption option, int unifiedLines, bool ignoreWhitespace, Models.DiffResult result)
{
Argument = option.ToString();
UnifiedLines = unifiedLines;
IgnoreWhitespace = ignoreWhitespace;
OldHash = result.OldHash;
NewHash = result.NewHash;
}
public bool IsSame(Info other)
{
return Argument.Equals(other.Argument, StringComparison.Ordinal) &&
UnifiedLines == other.UnifiedLines &&
IgnoreWhitespace == other.IgnoreWhitespace &&
OldHash.Equals(other.OldHash, StringComparison.Ordinal) &&
NewHash.Equals(other.NewHash, StringComparison.Ordinal);
}
}
private readonly string _repo; private readonly string _repo;
private readonly Models.DiffOption _option = null; private readonly Models.DiffOption _option = null;
private string _title; private string _title;
private string _fileModeChange = string.Empty; private string _fileModeChange = string.Empty;
private int _unifiedLines = 4; private int _unifiedLines = 4;
private bool _isLoading = true;
private bool _isTextDiff = false; private bool _isTextDiff = false;
private bool _ignoreWhitespace = false; private bool _ignoreWhitespace = false;
private object _content = null; private object _content = null;
private Info _info = null;
} }
} }

View file

@ -186,6 +186,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _showHiddenSymbolsInDiffView, value); set => SetProperty(ref _showHiddenSymbolsInDiffView, value);
} }
public bool UseFullTextDiff
{
get => _useFullTextDiff;
set => SetProperty(ref _useFullTextDiff, value);
}
public Models.ChangeViewMode UnstagedChangeViewMode public Models.ChangeViewMode UnstagedChangeViewMode
{ {
get => _unstagedChangeViewMode; get => _unstagedChangeViewMode;
@ -591,6 +597,7 @@ namespace SourceGit.ViewModels
private bool _useSyntaxHighlighting = false; private bool _useSyntaxHighlighting = false;
private bool _enableDiffViewWordWrap = false; private bool _enableDiffViewWordWrap = false;
private bool _showHiddenSymbolsInDiffView = false; private bool _showHiddenSymbolsInDiffView = false;
private bool _useFullTextDiff = false;
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List; private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;
private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List; private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List;

View file

@ -407,8 +407,8 @@ namespace SourceGit.Views
var menu = new ContextMenu(); var menu = new ContextMenu();
menu.Items.Add(copy); menu.Items.Add(copy);
menu.Open(TextArea.TextView);
TextArea.TextView.OpenContextMenu(menu);
e.Handled = true; e.Handled = true;
} }

View file

@ -15,7 +15,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.BranchCompare vm && sender is ChangeCollectionView view) if (DataContext is ViewModels.BranchCompare vm && sender is ChangeCollectionView view)
{ {
var menu = vm.CreateChangeContextMenu(); var menu = vm.CreateChangeContextMenu();
view.OpenContextMenu(menu); menu?.Open(view);
} }
e.Handled = true; e.Handled = true;

View file

@ -374,7 +374,7 @@ namespace SourceGit.Views
if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote })
{ {
var menu = repo.CreateContextMenuForRemote(remote); var menu = repo.CreateContextMenuForRemote(remote);
this.OpenContextMenu(menu); menu?.Open(this);
return; return;
} }
@ -391,7 +391,7 @@ namespace SourceGit.Views
var menu = branch.IsLocal ? var menu = branch.IsLocal ?
repo.CreateContextMenuForLocalBranch(branch) : repo.CreateContextMenuForLocalBranch(branch) :
repo.CreateContextMenuForRemoteBranch(branch); repo.CreateContextMenuForRemoteBranch(branch);
this.OpenContextMenu(menu); menu?.Open(this);
} }
else if (branches.Find(x => x.IsCurrent) == null) else if (branches.Find(x => x.IsCurrent) == null)
{ {
@ -405,7 +405,7 @@ namespace SourceGit.Views
ev.Handled = true; ev.Handled = true;
}; };
menu.Items.Add(deleteMulti); menu.Items.Add(deleteMulti);
this.OpenContextMenu(menu); menu?.Open(this);
} }
} }

View file

@ -68,7 +68,7 @@ namespace SourceGit.Views
private void OnOpenWebLink(object sender, RoutedEventArgs e) private void OnOpenWebLink(object sender, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.CommitDetail detail) if (DataContext is ViewModels.CommitDetail detail && sender is Control control)
{ {
var links = WebLinks; var links = WebLinks;
if (links.Count > 1) if (links.Count > 1)
@ -88,7 +88,7 @@ namespace SourceGit.Views
menu.Items.Add(item); menu.Items.Add(item);
} }
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
} }
else if (links.Count == 1) else if (links.Count == 1)
{ {

View file

@ -16,7 +16,7 @@ namespace SourceGit.Views
DataContext is ViewModels.CommitDetail vm) DataContext is ViewModels.CommitDetail vm)
{ {
var menu = vm.CreateChangeContextMenu(selected[0]); var menu = vm.CreateChangeContextMenu(selected[0]);
view.OpenContextMenu(menu); menu?.Open(view);
} }
e.Handled = true; e.Handled = true;

View file

@ -26,7 +26,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change) if (DataContext is ViewModels.CommitDetail detail && sender is Grid grid && grid.DataContext is Models.Change change)
{ {
var menu = detail.CreateChangeContextMenu(change); var menu = detail.CreateChangeContextMenu(change);
grid.OpenContextMenu(menu); menu?.Open(grid);
} }
e.Handled = true; e.Handled = true;

View file

@ -176,7 +176,38 @@ namespace SourceGit.Views
} }
else else
{ {
Native.OS.OpenBrowser(_lastHover.Link); var point = e.GetCurrentPoint(this);
var link = _lastHover.Link;
if (point.Properties.IsLeftButtonPressed)
{
Native.OS.OpenBrowser(link);
}
else if (point.Properties.IsRightButtonPressed)
{
var open = new MenuItem();
open.Header = App.Text("IssueLinkCM.OpenInBrowser");
open.Icon = App.CreateMenuIcon("Icons.OpenWith");
open.Click += (_, ev) =>
{
Native.OS.OpenBrowser(link);
ev.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("IssueLinkCM.CopyLink");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (_, ev) =>
{
App.CopyText(link);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(open);
menu.Items.Add(copy);
menu.Open(this);
}
} }
e.Handled = true; e.Handled = true;

View file

@ -1,26 +0,0 @@
using System.ComponentModel;
using Avalonia.Controls;
namespace SourceGit.Views
{
public static class ContextMenuExtension
{
public static void OpenContextMenu(this Control control, ContextMenu menu)
{
if (menu == null)
return;
menu.PlacementTarget = control;
menu.Closing += OnContextMenuClosing; // Clear context menu because it is dynamic.
control.ContextMenu = menu;
control.ContextMenu?.Open();
}
private static void OnContextMenuClosing(object sender, CancelEventArgs e)
{
if (sender is ContextMenu menu && menu.PlacementTarget != null)
menu.PlacementTarget.ContextMenu = null;
}
}
}

View file

@ -39,6 +39,9 @@
Command="{Binding IncrUnified}" Command="{Binding IncrUnified}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Incr}"> ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Incr}">
<Button.IsEnabled>
<Binding Source="{x:Static vm:Preference.Instance}" Path="UseFullTextDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</Button.IsEnabled>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Incr}"/> <Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Incr}"/>
</Button> </Button>
@ -46,10 +49,26 @@
Width="32" Width="32"
Command="{Binding DecrUnified}" Command="{Binding DecrUnified}"
IsVisible="{Binding IsTextDiff}" IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Decr}" ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.Decr}">
IsEnabled="{Binding UnifiedLines, Converter={x:Static c:IntConverters.IsGreaterThanFour}}"> <Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="UnifiedLines" Mode="OneWay" Converter="{x:Static c:IntConverters.IsGreaterThanFour}"/>
<Binding Source="{x:Static vm:Preference.Instance}" Path="UseFullTextDiff" Mode="OneWay" Converter="{x:Static BoolConverters.Not}"/>
</MultiBinding>
</Button.IsEnabled>
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Decr}"/> <Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Lines.Decr}"/>
</Button> </Button>
<ToggleButton Classes="line_path"
Width="32" Height="18"
Background="Transparent"
Padding="9,6"
Command="{Binding ToggleFullTextDiff}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.VisualLines.All}">
<Path Width="13" Height="13" Data="{StaticResource Icons.Lines.All}" Margin="0,3,0,0"/>
</ToggleButton>
<ToggleButton Classes="line_path" <ToggleButton Classes="line_path"
Width="32" Height="18" Width="32" Height="18"
@ -211,7 +230,9 @@
<!-- Text Diff --> <!-- Text Diff -->
<DataTemplate DataType="m:TextDiff"> <DataTemplate DataType="m:TextDiff">
<v:TextDiffView UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"/> <v:TextDiffView
UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
UseFullTextDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"/>
</DataTemplate> </DataTemplate>
<!-- Empty or only EOL changes --> <!-- Empty or only EOL changes -->

View file

@ -706,7 +706,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.Histories histories && sender is ListBox { SelectedItems: { Count: > 0 } } list) if (DataContext is ViewModels.Histories histories && sender is ListBox { SelectedItems: { Count: > 0 } } list)
{ {
var menu = histories.MakeContextMenu(list); var menu = histories.MakeContextMenu(list);
list.OpenContextMenu(menu); menu?.Open(list);
} }
e.Handled = true; e.Handled = true;
} }

View file

@ -250,7 +250,7 @@ namespace SourceGit.Views
if (sender is Button btn && DataContext is ViewModels.Launcher launcher) if (sender is Button btn && DataContext is ViewModels.Launcher launcher)
{ {
var menu = launcher.CreateContextForWorkspace(); var menu = launcher.CreateContextForWorkspace();
btn.OpenContextMenu(menu); menu?.Open(btn);
} }
e.Handled = true; e.Handled = true;

View file

@ -234,7 +234,7 @@ namespace SourceGit.Views
if (sender is Border border && DataContext is ViewModels.Launcher vm) if (sender is Border border && DataContext is ViewModels.Launcher vm)
{ {
var menu = vm.CreateContextForPageTab(border.DataContext as ViewModels.LauncherPage); var menu = vm.CreateContextForPageTab(border.DataContext as ViewModels.LauncherPage);
border.OpenContextMenu(menu); menu?.Open(border);
} }
e.Handled = true; e.Handled = true;

View file

@ -189,7 +189,7 @@ namespace SourceGit.Views
if (sender is ListBox { SelectedItem: Models.Submodule submodule } grid && DataContext is ViewModels.Repository repo) if (sender is ListBox { SelectedItem: Models.Submodule submodule } grid && DataContext is ViewModels.Repository repo)
{ {
var menu = repo.CreateContextMenuForSubmodule(submodule.Path); var menu = repo.CreateContextMenuForSubmodule(submodule.Path);
grid.OpenContextMenu(menu); menu?.Open(grid);
} }
e.Handled = true; e.Handled = true;
@ -210,7 +210,7 @@ namespace SourceGit.Views
if (sender is ListBox { SelectedItem: Models.Worktree worktree } grid && DataContext is ViewModels.Repository repo) if (sender is ListBox { SelectedItem: Models.Worktree worktree } grid && DataContext is ViewModels.Repository repo)
{ {
var menu = repo.CreateContextMenuForWorktree(worktree); var menu = repo.CreateContextMenuForWorktree(worktree);
grid.OpenContextMenu(menu); menu?.Open(grid);
} }
e.Handled = true; e.Handled = true;

View file

@ -17,7 +17,7 @@ namespace SourceGit.Views
if (sender is Button button && DataContext is ViewModels.Repository repo) if (sender is Button button && DataContext is ViewModels.Repository repo)
{ {
var menu = repo.CreateContextMenuForExternalTools(); var menu = repo.CreateContextMenuForExternalTools();
button.OpenContextMenu(menu); menu?.Open(button);
e.Handled = true; e.Handled = true;
} }
} }
@ -72,10 +72,10 @@ namespace SourceGit.Views
private void OpenGitFlowMenu(object sender, RoutedEventArgs e) private void OpenGitFlowMenu(object sender, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.Repository repo) if (DataContext is ViewModels.Repository repo && sender is Control control)
{ {
var menu = repo.CreateContextMenuForGitFlow(); var menu = repo.CreateContextMenuForGitFlow();
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
} }
e.Handled = true; e.Handled = true;
@ -83,10 +83,10 @@ namespace SourceGit.Views
private void OpenGitLFSMenu(object sender, RoutedEventArgs e) private void OpenGitLFSMenu(object sender, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.Repository repo) if (DataContext is ViewModels.Repository repo && sender is Control control)
{ {
var menu = repo.CreateContextMenuForGitLFS(); var menu = repo.CreateContextMenuForGitLFS();
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
} }
e.Handled = true; e.Handled = true;
@ -94,10 +94,10 @@ namespace SourceGit.Views
private void OpenCustomActionMenu(object sender, RoutedEventArgs e) private void OpenCustomActionMenu(object sender, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.Repository repo) if (DataContext is ViewModels.Repository repo && sender is Control control)
{ {
var menu = repo.CreateContextMenuForCustomAction(); var menu = repo.CreateContextMenuForCustomAction();
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
} }
e.Handled = true; e.Handled = true;

View file

@ -15,7 +15,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.RevisionCompare vm && sender is ChangeCollectionView view) if (DataContext is ViewModels.RevisionCompare vm && sender is ChangeCollectionView view)
{ {
var menu = vm.CreateChangeContextMenu(); var menu = vm.CreateChangeContextMenu();
view.OpenContextMenu(menu); menu?.Open(view);
} }
e.Handled = true; e.Handled = true;

View file

@ -229,7 +229,7 @@ namespace SourceGit.Views
if (obj.Type != Models.ObjectType.Tree) if (obj.Type != Models.ObjectType.Tree)
{ {
var menu = vm.CreateRevisionFileContextMenu(obj); var menu = vm.CreateRevisionFileContextMenu(obj);
grid.OpenContextMenu(menu); menu?.Open(grid);
} }
} }

View file

@ -95,8 +95,8 @@ namespace SourceGit.Views
var menu = new ContextMenu(); var menu = new ContextMenu();
menu.Items.Add(copy); menu.Items.Add(copy);
menu.Open(TextArea.TextView);
TextArea.TextView.OpenContextMenu(menu);
e.Handled = true; e.Handled = true;
} }

View file

@ -28,7 +28,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.StashesPage vm && sender is Border border) if (DataContext is ViewModels.StashesPage vm && sender is Border border)
{ {
var menu = vm.MakeContextMenu(border.DataContext as Models.Stash); var menu = vm.MakeContextMenu(border.DataContext as Models.Stash);
border.OpenContextMenu(menu); menu?.Open(border);
} }
e.Handled = true; e.Handled = true;
} }
@ -38,7 +38,7 @@ namespace SourceGit.Views
if (DataContext is ViewModels.StashesPage vm && sender is Grid grid) if (DataContext is ViewModels.StashesPage vm && sender is Grid grid)
{ {
var menu = vm.MakeContextMenuForChange(grid.DataContext as Models.Change); var menu = vm.MakeContextMenuForChange(grid.DataContext as Models.Change);
grid.OpenContextMenu(menu); menu?.Open(grid);
} }
e.Handled = true; e.Handled = true;
} }

View file

@ -225,7 +225,7 @@ namespace SourceGit.Views
if (selected != null && DataContext is ViewModels.Repository repo) if (selected != null && DataContext is ViewModels.Repository repo)
{ {
var menu = repo.CreateContextMenuForTag(selected); var menu = repo.CreateContextMenuForTag(selected);
control.OpenContextMenu(menu); menu?.Open(control);
} }
e.Handled = true; e.Handled = true;

View file

@ -589,8 +589,8 @@ namespace SourceGit.Views
var menu = new ContextMenu(); var menu = new ContextMenu();
menu.Items.Add(copy); menu.Items.Add(copy);
menu.Open(TextArea.TextView);
TextArea.TextView.OpenContextMenu(menu);
e.Handled = true; e.Handled = true;
} }
@ -902,7 +902,7 @@ namespace SourceGit.Views
var scroller = this.FindDescendantOfType<ScrollViewer>(); var scroller = this.FindDescendantOfType<ScrollViewer>();
if (scroller != null) if (scroller != null)
{ {
scroller.Bind(ScrollViewer.OffsetProperty, new Binding("SyncScrollOffset", BindingMode.TwoWay)); scroller.Bind(ScrollViewer.OffsetProperty, new Binding("ScrollOffset", BindingMode.TwoWay));
scroller.GotFocus += OnTextViewScrollGotFocus; scroller.GotFocus += OnTextViewScrollGotFocus;
} }
} }
@ -1205,6 +1205,15 @@ namespace SourceGit.Views
set => SetValue(UseSideBySideDiffProperty, value); set => SetValue(UseSideBySideDiffProperty, value);
} }
public static readonly StyledProperty<bool> UseFullTextDiffProperty =
AvaloniaProperty.Register<TextDiffView, bool>(nameof(UseFullTextDiff));
public bool UseFullTextDiff
{
get => GetValue(UseFullTextDiffProperty);
set => SetValue(UseFullTextDiffProperty, value);
}
public static readonly StyledProperty<TextDiffViewChunk> SelectedChunkProperty = public static readonly StyledProperty<TextDiffViewChunk> SelectedChunkProperty =
AvaloniaProperty.Register<TextDiffView, TextDiffViewChunk>(nameof(SelectedChunk)); AvaloniaProperty.Register<TextDiffView, TextDiffViewChunk>(nameof(SelectedChunk));
@ -1236,15 +1245,12 @@ namespace SourceGit.Views
{ {
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) => UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
{ {
if (v.DataContext is Models.TextDiff diff) v.RefreshContent(v.DataContext as Models.TextDiff, false);
{ });
diff.SyncScrollOffset = Vector.Zero;
if (v.UseSideBySideDiff) UseFullTextDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
v.Editor.Content = new ViewModels.TwoSideTextDiff(diff); {
else v.RefreshContent(v.DataContext as Models.TextDiff, false);
v.Editor.Content = diff;
}
}); });
SelectedChunkProperty.Changed.AddClassHandler<TextDiffView>((v, _) => SelectedChunkProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
@ -1271,25 +1277,7 @@ namespace SourceGit.Views
protected override void OnDataContextChanged(EventArgs e) protected override void OnDataContextChanged(EventArgs e)
{ {
base.OnDataContextChanged(e); base.OnDataContextChanged(e);
RefreshContent(DataContext as Models.TextDiff, true);
if (SelectedChunk != null)
SetCurrentValue(SelectedChunkProperty, null);
var diff = DataContext as Models.TextDiff;
if (diff == null)
{
Editor.Content = null;
GC.Collect();
return;
}
if (UseSideBySideDiff)
Editor.Content = new ViewModels.TwoSideTextDiff(diff, Editor.Content as ViewModels.TwoSideTextDiff);
else
Editor.Content = diff;
IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null;
} }
protected override void OnPointerExited(PointerEventArgs e) protected override void OnPointerExited(PointerEventArgs e)
@ -1300,6 +1288,34 @@ namespace SourceGit.Views
SetCurrentValue(SelectedChunkProperty, null); SetCurrentValue(SelectedChunkProperty, null);
} }
private void RefreshContent(Models.TextDiff diff, bool keepScrollOffset = true)
{
if (SelectedChunk != null)
SetCurrentValue(SelectedChunkProperty, null);
if (diff == null)
{
Editor.Content = null;
GC.Collect();
return;
}
if (UseSideBySideDiff)
{
var previousContent = Editor.Content as ViewModels.TwoSideTextDiff;
Editor.Content = new ViewModels.TwoSideTextDiff(diff, keepScrollOffset ? previousContent : null);
}
else
{
if (!keepScrollOffset)
diff.ScrollOffset = Vector.Zero;
Editor.Content = diff;
}
IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null;
}
private void OnStageChunk(object _1, RoutedEventArgs _2) private void OnStageChunk(object _1, RoutedEventArgs _2)
{ {
var chunk = SelectedChunk; var chunk = SelectedChunk;

View file

@ -117,7 +117,7 @@ namespace SourceGit.Views
if (sender is Grid { DataContext: ViewModels.RepositoryNode node } grid) if (sender is Grid { DataContext: ViewModels.RepositoryNode node } grid)
{ {
var menu = ViewModels.Welcome.Instance.CreateContextMenu(node); var menu = ViewModels.Welcome.Instance.CreateContextMenu(node);
grid.OpenContextMenu(menu); menu?.Open(grid);
e.Handled = true; e.Handled = true;
} }
} }

View file

@ -31,27 +31,27 @@ namespace SourceGit.Views
{ {
var menu = vm.CreateContextMenuForCommitMessages(); var menu = vm.CreateContextMenuForCommitMessages();
menu.Placement = PlacementMode.TopEdgeAlignedLeft; menu.Placement = PlacementMode.TopEdgeAlignedLeft;
button.OpenContextMenu(menu); menu?.Open(button);
e.Handled = true; e.Handled = true;
} }
} }
private void OnUnstagedContextRequested(object sender, ContextRequestedEventArgs e) private void OnUnstagedContextRequested(object sender, ContextRequestedEventArgs e)
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm && sender is Control control)
{ {
var menu = vm.CreateContextMenuForUnstagedChanges(); var menu = vm.CreateContextMenuForUnstagedChanges();
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
e.Handled = true; e.Handled = true;
} }
} }
private void OnStagedContextRequested(object sender, ContextRequestedEventArgs e) private void OnStagedContextRequested(object sender, ContextRequestedEventArgs e)
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm && sender is Control control)
{ {
var menu = vm.CreateContextMenuForStagedChanges(); var menu = vm.CreateContextMenuForStagedChanges();
(sender as Control)?.OpenContextMenu(menu); menu?.Open(control);
e.Handled = true; e.Handled = true;
} }
} }
@ -136,10 +136,10 @@ namespace SourceGit.Views
private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e) private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm && sender is Control control)
{ {
var menu = vm.CreateContextForOpenAI(); var menu = vm.CreateContextForOpenAI();
(sender as Button)?.OpenContextMenu(menu); menu?.Open(control);
} }
e.Handled = true; e.Handled = true;