feature: supports to open file with detected external tools (#1882)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-10-30 18:13:49 +08:00
parent 5b818b6e8c
commit fa97d44e60
13 changed files with 151 additions and 65 deletions

View File

@@ -20,7 +20,7 @@ namespace SourceGit.Models
{
Name = name;
ExecFile = execFile;
_execArgsGenerator = execArgsGenerator ?? (repo => repo.Quoted());
_execArgsGenerator = execArgsGenerator ?? (path => path.Quoted());
try
{
@@ -34,13 +34,12 @@ namespace SourceGit.Models
}
}
public void Open(string repo)
public void Open(string path)
{
Process.Start(new ProcessStartInfo()
{
WorkingDirectory = repo,
FileName = ExecFile,
Arguments = _execArgsGenerator.Invoke(repo),
Arguments = _execArgsGenerator.Invoke(path),
UseShellExecute = false,
});
}

View File

@@ -449,10 +449,15 @@ namespace SourceGit.Native
}
}
private string GenerateCommandlineArgsForVisualStudio(string repo)
private string GenerateCommandlineArgsForVisualStudio(string path)
{
var sln = FindVSSolutionFile(new DirectoryInfo(repo), 4);
return string.IsNullOrEmpty(sln) ? repo.Quoted() : sln.Quoted();
if (Directory.Exists(path))
{
var sln = FindVSSolutionFile(new DirectoryInfo(path), 4);
return string.IsNullOrEmpty(sln) ? path.Quoted() : sln.Quoted();
}
return path.Quoted();
}
private string FindVSSolutionFile(DirectoryInfo dir, int leftDepth)

View File

@@ -536,9 +536,10 @@
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">Select parent node for:</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 [Preferences] and configure it first.</x:String>
<x:String x:Key="Text.Open" xml:space="preserve">Open</x:String>
<x:String x:Key="Text.Open.SystemDefaultEditor" xml:space="preserve">Default Editor (System)</x:String>
<x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">Open Data Storage Directory</x:String>
<x:String x:Key="Text.OpenInExternalMergeTool" xml:space="preserve">Open in Merge Tool</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.PageTabBar.New" xml:space="preserve">Create New Tab</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">Bookmark</x:String>

View File

@@ -540,9 +540,10 @@
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">请选择目标分组:</x:String>
<x:String x:Key="Text.Name" xml:space="preserve">名称 </x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">GIT尚未配置。请打开【偏好设置】配置GIT路径。</x:String>
<x:String x:Key="Text.Open" xml:space="preserve">打开</x:String>
<x:String x:Key="Text.Open.SystemDefaultEditor" xml:space="preserve">系统默认编辑器</x:String>
<x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">浏览应用数据目录</x:String>
<x:String x:Key="Text.OpenInExternalMergeTool" xml:space="preserve">使用外部对比工具查看</x:String>
<x:String x:Key="Text.OpenWith" xml:space="preserve">打开文件...</x:String>
<x:String x:Key="Text.Optional" xml:space="preserve">选填。</x:String>
<x:String x:Key="Text.PageTabBar.New" xml:space="preserve">新建空白页</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">设置书签</x:String>

View File

@@ -540,9 +540,10 @@
<x:String x:Key="Text.MoveRepositoryNode.Target" xml:space="preserve">請選擇目標分組:</x:String>
<x:String x:Key="Text.Name" xml:space="preserve">名稱:</x:String>
<x:String x:Key="Text.NotConfigured" xml:space="preserve">尚未設定 Git。請開啟 [偏好設定] 以設定 Git 路徑。</x:String>
<x:String x:Key="Text.Open" xml:space="preserve">開啟</x:String>
<x:String x:Key="Text.Open.SystemDefaultEditor" xml:space="preserve">系統預設編輯器</x:String>
<x:String x:Key="Text.OpenAppDataDir" xml:space="preserve">瀏覽程式資料目錄</x:String>
<x:String x:Key="Text.OpenInExternalMergeTool" xml:space="preserve">使用外部比對工具檢視</x:String>
<x:String x:Key="Text.OpenWith" xml:space="preserve">開啟檔案...</x:String>
<x:String x:Key="Text.Optional" xml:space="preserve">選填。</x:String>
<x:String x:Key="Text.PageTabBar.New" xml:space="preserve">新增分頁</x:String>
<x:String x:Key="Text.PageTabBar.Tab.Bookmark" xml:space="preserve">設定書籤</x:String>

View File

@@ -318,7 +318,7 @@ namespace SourceGit.ViewModels
}
}
public async Task OpenRevisionFileWithDefaultEditorAsync(string file)
public async Task OpenRevisionFileAsync(string file, Models.ExternalTool tool)
{
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file);
var fileName = Path.GetFileNameWithoutExtension(fullPath) ?? "";
@@ -329,7 +329,10 @@ namespace SourceGit.ViewModels
.RunAsync(_repo.FullPath, _commit.SHA, file, tmpFile)
.ConfigureAwait(false);
Native.OS.OpenWithDefaultEditor(tmpFile);
if (tool == null)
Native.OS.OpenWithDefaultEditor(tmpFile);
else
tool.Open(tmpFile);
}
public async Task SaveRevisionFileAsync(Models.Object file, string saveTo)

View File

@@ -335,13 +335,6 @@ namespace SourceGit.ViewModels
});
}
public void OpenWithDefaultEditor(Models.Change c)
{
var absPath = Native.OS.GetAbsPath(_repo.FullPath, c.Path);
if (File.Exists(absPath))
Native.OS.OpenWithDefaultEditor(absPath);
}
public async Task StageChangesAsync(List<Models.Change> changes, Models.Change next)
{
var canStaged = await GetCanStageChangesAsync(changes);

View File

@@ -209,6 +209,44 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.CommitDetail { Repository: { } repo, Commit: { } commit } vm)
return null;
var openWith = new MenuItem();
openWith.Header = App.Text("Open");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = change.Index != Models.ChangeState.Deleted;
if (openWith.IsEnabled)
{
var defaultEditor = new MenuItem();
defaultEditor.Header = App.Text("Open.SystemDefaultEditor");
defaultEditor.Click += async (_, ev) =>
{
await vm.OpenRevisionFileAsync(change.Path, null);
ev.Handled = true;
};
openWith.Items.Add(defaultEditor);
var tools = Native.OS.ExternalTools;
if (tools.Count > 0)
{
openWith.Items.Add(new MenuItem() { Header = "-" });
for (var i = 0; i < tools.Count; i++)
{
var tool = tools[i];
var item = new MenuItem();
item.Header = tool.Name;
item.Icon = new Image { Width = 16, Height = 16, Source = tool.IconImage };
item.Click += async (_, ev) =>
{
await vm.OpenRevisionFileAsync(change.Path, tool);
ev.Handled = true;
};
openWith.Items.Add(item);
}
}
}
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
@@ -219,16 +257,6 @@ namespace SourceGit.Views
ev.Handled = true;
};
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = change.Index != Models.ChangeState.Deleted;
openWith.Click += async (_, ev) =>
{
await vm.OpenRevisionFileWithDefaultEditorAsync(change.Path);
ev.Handled = true;
};
var fullPath = Native.OS.GetAbsPath(repo.FullPath, change.Path);
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
@@ -291,8 +319,8 @@ namespace SourceGit.Views
};
var menu = new ContextMenu();
menu.Items.Add(openWithMerger);
menu.Items.Add(openWith);
menu.Items.Add(openWithMerger);
menu.Items.Add(explore);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(history);

View File

@@ -169,7 +169,7 @@
Background="Transparent"
Click="OnOpenFileWithDefaultEditor"
IsVisible="{Binding CanOpenWithDefaultEditor, Mode=OneWay}"
ToolTip.Tip="{DynamicResource Text.OpenWith}">
ToolTip.Tip="{DynamicResource Text.Open}">
<Path Width="12" Height="12" Data="{StaticResource Icons.OpenWith}"/>
</Button>
</Grid>

View File

@@ -495,15 +495,43 @@ namespace SourceGit.Views
var menu = new ContextMenu();
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Header = App.Text("Open");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
openWith.IsEnabled = file.Type == Models.ObjectType.Blob;
openWith.Click += async (_, ev) =>
if (openWith.IsEnabled)
{
await vm.OpenRevisionFileWithDefaultEditorAsync(file.Path);
ev.Handled = true;
};
var defaultEditor = new MenuItem();
defaultEditor.Header = App.Text("Open.SystemDefaultEditor");
defaultEditor.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
defaultEditor.Click += async (_, ev) =>
{
await vm.OpenRevisionFileAsync(file.Path, null);
ev.Handled = true;
};
openWith.Items.Add(defaultEditor);
var tools = Native.OS.ExternalTools;
if (tools.Count > 0)
{
openWith.Items.Add(new MenuItem() { Header = "-" });
for (var i = 0; i < tools.Count; i++)
{
var tool = tools[i];
var item = new MenuItem();
item.Header = tool.Name;
item.Icon = new Image { Width = 16, Height = 16, Source = tool.IconImage };
item.Click += async (_, ev) =>
{
await vm.OpenRevisionFileAsync(file.Path, tool);
ev.Handled = true;
};
openWith.Items.Add(item);
}
}
}
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");

View File

@@ -151,7 +151,7 @@
HotKey="{OnPlatform Ctrl+O, macOS=⌘+O}">
<ToolTip.Tip>
<TextBlock>
<Run Text="{DynamicResource Text.OpenWith}"/>
<Run Text="{DynamicResource Text.Open}"/>
<Run Text=" "/>
<Run Text="{OnPlatform Ctrl+O, macOS=⌘+O}" FontSize="11" Foreground="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"/>
</TextBlock>

View File

@@ -82,7 +82,7 @@ namespace SourceGit.Views
private async void OnOpenFileWithDefaultEditor(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.CommitDetail { CanOpenRevisionFileWithDefaultEditor: true } vm)
await vm.OpenRevisionFileWithDefaultEditorAsync(vm.ViewRevisionFilePath);
await vm.OpenRevisionFileAsync(vm.ViewRevisionFilePath, null);
e.Handled = true;
}

View File

@@ -110,7 +110,10 @@ namespace SourceGit.Views
e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) &&
vm.SelectedUnstaged is { Count: 1 })
{
vm.OpenWithDefaultEditor(vm.SelectedUnstaged[0]);
var change = vm.SelectedUnstaged[0];
var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path);
if (File.Exists(fullpath))
Native.OS.OpenWithDefaultEditor(fullpath);
e.Handled = true;
}
else if (e.Key is Key.C &&
@@ -143,7 +146,10 @@ namespace SourceGit.Views
e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) &&
vm.SelectedStaged is { Count: 1 })
{
vm.OpenWithDefaultEditor(vm.SelectedStaged[0]);
var change = vm.SelectedStaged[0];
var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path);
if (File.Exists(fullpath))
Native.OS.OpenWithDefaultEditor(fullpath);
e.Handled = true;
}
else if (e.Key is Key.C &&
@@ -254,6 +260,7 @@ namespace SourceGit.Views
{
var change = selectedUnstaged[0];
var path = Native.OS.GetAbsPath(repo.FullPath, change.Path);
TryAddOpenFileToContextMenu(menu, path);
if (!change.IsConflicted || change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified)
{
@@ -273,18 +280,6 @@ namespace SourceGit.Views
menu.Items.Add(openMerger);
}
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) =>
{
vm.OpenWithDefaultEditor(change);
e.Handled = true;
};
menu.Items.Add(openWith);
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
@@ -934,17 +929,6 @@ namespace SourceGit.Views
ev.Handled = true;
};
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) =>
{
vm.OpenWithDefaultEditor(change);
e.Handled = true;
};
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
@@ -1005,8 +989,8 @@ namespace SourceGit.Views
e.Handled = true;
};
TryAddOpenFileToContextMenu(menu, path);
menu.Items.Add(openWithMerger);
menu.Items.Add(openWith);
menu.Items.Add(explore);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(unstage);
@@ -1274,6 +1258,49 @@ namespace SourceGit.Views
return menu;
}
private void TryAddOpenFileToContextMenu(ContextMenu menu, string fullpath)
{
var openWith = new MenuItem();
openWith.Header = App.Text("Open");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = File.Exists(fullpath);
if (openWith.IsEnabled)
{
var defaultEditor = new MenuItem();
defaultEditor.Header = App.Text("Open.SystemDefaultEditor");
defaultEditor.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
defaultEditor.Click += (_, ev) =>
{
Native.OS.OpenWithDefaultEditor(fullpath);
ev.Handled = true;
};
openWith.Items.Add(defaultEditor);
var tools = Native.OS.ExternalTools;
if (tools.Count > 0)
{
openWith.Items.Add(new MenuItem() { Header = "-" });
for (var i = 0; i < tools.Count; i++)
{
var tool = tools[i];
var item = new MenuItem();
item.Header = tool.Name;
item.Icon = new Image { Width = 16, Height = 16, Source = tool.IconImage };
item.Click += (_, e) =>
{
tool.Open(fullpath);
e.Handled = true;
};
openWith.Items.Add(item);
}
}
}
menu.Items.Add(openWith);
}
private void TryToAddCustomActionsToContextMenu(ViewModels.Repository repo, ContextMenu menu, string path)
{
var actions = repo.GetCustomActions(Models.CustomActionScope.File);