From 894f3e9b033f15df5f87f251299afa6ce9428227 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 2 Dec 2024 21:44:15 +0800 Subject: [PATCH] feature: supports searching revision files (#775) --- ...sionFiles.cs => QueryRevisionFileNames.cs} | 8 +- src/Commands/QueryRevisionObjects.cs | 22 ++++- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/CommitDetail.cs | 95 ++++++++++++++++++- src/ViewModels/Repository.cs | 2 +- src/Views/RevisionFileTreeView.axaml.cs | 64 +++++++++++++ src/Views/RevisionFiles.axaml | 90 +++++++++++++++++- src/Views/RevisionFiles.axaml.cs | 81 ++++++++++++++++ 10 files changed, 350 insertions(+), 15 deletions(-) rename src/Commands/{QueryCurrentRevisionFiles.cs => QueryRevisionFileNames.cs} (55%) diff --git a/src/Commands/QueryCurrentRevisionFiles.cs b/src/Commands/QueryRevisionFileNames.cs similarity index 55% rename from src/Commands/QueryCurrentRevisionFiles.cs rename to src/Commands/QueryRevisionFileNames.cs index 217ea20e..d2d69614 100644 --- a/src/Commands/QueryCurrentRevisionFiles.cs +++ b/src/Commands/QueryRevisionFileNames.cs @@ -1,19 +1,19 @@ namespace SourceGit.Commands { - public class QueryCurrentRevisionFiles : Command + public class QueryRevisionFileNames : Command { - public QueryCurrentRevisionFiles(string repo) + public QueryRevisionFileNames(string repo, string revision) { WorkingDirectory = repo; Context = repo; - Args = "ls-tree -r --name-only HEAD"; + Args = $"ls-tree -r -z --name-only {revision}"; } public string[] Result() { var rs = ReadToEnd(); if (rs.IsSuccess) - return rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries); + return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries); return []; } diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs index bcad9129..de3406e8 100644 --- a/src/Commands/QueryRevisionObjects.cs +++ b/src/Commands/QueryRevisionObjects.cs @@ -12,7 +12,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = $"ls-tree {sha}"; + Args = $"ls-tree -z {sha}"; if (!string.IsNullOrEmpty(parentFolder)) Args += $" -- \"{parentFolder}\""; @@ -20,11 +20,27 @@ namespace SourceGit.Commands public List Result() { - Exec(); + var rs = ReadToEnd(); + if (rs.IsSuccess) + { + var start = 0; + var end = rs.StdOut.IndexOf('\0', start); + while (end > 0) + { + var line = rs.StdOut.Substring(start, end - start); + Parse(line); + start = end + 1; + end = rs.StdOut.IndexOf('\0', start); + } + + if (start < rs.StdOut.Length) + Parse(rs.StdOut.Substring(start)); + } + return _objects; } - protected override void OnReadline(string line) + private void Parse(string line) { var match = REG_FORMAT().Match(line); if (!match.Success) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d06635eb..6bea386d 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -121,6 +121,7 @@ Search Changes... FILES LFS File + Search Files... Submodule INFORMATION AUTHOR diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 9cf0eedf..56a5353e 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -124,6 +124,7 @@ 查找变更... 文件列表 LFS文件 + 查找文件... 子模块 基本信息 修改者 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 920d9114..61029b45 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -124,6 +124,7 @@ 搜尋變更... 檔案列表 LFS 檔案 + 搜尋檔案... 子模組 基本資訊 作者 diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index f14b0359..ec2822cd 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -82,7 +82,7 @@ namespace SourceGit.ViewModels { get; private set; - } = new AvaloniaList(); + } = []; public string SearchChangeFilter { @@ -106,13 +106,68 @@ namespace SourceGit.ViewModels { get; private set; - } = new AvaloniaList(); + } = []; public AvaloniaList IssueTrackerRules { get => _repo.Settings?.IssueTrackerRules; } + public string RevisionFileSearchFilter + { + get => _revisionFileSearchFilter; + set + { + if (SetProperty(ref _revisionFileSearchFilter, value)) + { + RevisionFileSearchSuggestion.Clear(); + + if (!string.IsNullOrEmpty(value)) + { + if (_revisionFiles.Count == 0) + { + var sha = Commit.SHA; + + Task.Run(() => + { + var files = new Commands.QueryRevisionFileNames(_repo.FullPath, sha).Result(); + + Dispatcher.UIThread.Invoke(() => { + if (sha == Commit.SHA) + { + _revisionFiles.Clear(); + _revisionFiles.AddRange(files); + UpdateRevisionFileSearchSuggestion(); + } + }); + }); + } + else + { + UpdateRevisionFileSearchSuggestion(); + } + } + else + { + IsRevisionFileSearchSuggestionOpen = false; + GC.Collect(); + } + } + } + } + + public AvaloniaList RevisionFileSearchSuggestion + { + get; + private set; + } = []; + + public bool IsRevisionFileSearchSuggestionOpen + { + get => _isRevisionFileSearchSuggestionOpen; + set => SetProperty(ref _isRevisionFileSearchSuggestionOpen, value); + } + public CommitDetail(Repository repo) { _repo = repo; @@ -147,17 +202,23 @@ namespace SourceGit.ViewModels { _repo = null; _commit = null; + if (_changes != null) _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); if (_selectedChanges != null) _selectedChanges.Clear(); + _signInfo = null; _searchChangeFilter = null; _diffContext = null; _viewRevisionFileContent = null; _cancelToken = null; + + WebLinks.Clear(); + _revisionFiles.Clear(); + RevisionFileSearchSuggestion.Clear(); } public void NavigateTo(string commitSHA) @@ -175,6 +236,11 @@ namespace SourceGit.ViewModels SearchChangeFilter = string.Empty; } + public void ClearRevisionFileSearchFilter() + { + RevisionFileSearchFilter = string.Empty; + } + public Models.Commit GetParent(string sha) { return new Commands.QuerySingleCommit(_repo.FullPath, sha).Result(); @@ -543,6 +609,8 @@ namespace SourceGit.ViewModels private void Refresh() { _changes = null; + _revisionFiles.Clear(); + FullMessage = string.Empty; SignInfo = null; Changes = []; @@ -550,6 +618,8 @@ namespace SourceGit.ViewModels SelectedChanges = null; ViewRevisionFileContent = null; Children.Clear(); + RevisionFileSearchFilter = string.Empty; + IsRevisionFileSearchSuggestionOpen = false; if (_commit == null) return; @@ -716,6 +786,24 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); } + private void UpdateRevisionFileSearchSuggestion() + { + var suggestion = new List(); + foreach (var file in _revisionFiles) + { + if (file.Contains(_revisionFileSearchFilter, StringComparison.OrdinalIgnoreCase) && + file.Length != _revisionFileSearchFilter.Length) + suggestion.Add(file); + + if (suggestion.Count >= 100) + break; + } + + RevisionFileSearchSuggestion.Clear(); + RevisionFileSearchSuggestion.AddRange(suggestion); + IsRevisionFileSearchSuggestionOpen = suggestion.Count > 0; + } + [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] private static partial Regex REG_LFS_FORMAT(); @@ -736,5 +824,8 @@ namespace SourceGit.ViewModels private DiffContext _diffContext = null; private object _viewRevisionFileContent = null; private Commands.Command.CancelToken _cancelToken = null; + private List _revisionFiles = []; + private string _revisionFileSearchFilter = string.Empty; + private bool _isRevisionFileSearchSuggestionOpen = false; } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 92bfb288..514bdb6d 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2147,7 +2147,7 @@ namespace SourceGit.ViewModels { Task.Run(() => { - var files = new Commands.QueryCurrentRevisionFiles(_fullpath).Result(); + var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); Dispatcher.UIThread.Invoke(() => { if (_searchCommitFilterType != 3) diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index af9beb7d..b671851c 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -144,6 +144,68 @@ namespace SourceGit.Views InitializeComponent(); } + public void SetSearchResult(string file) + { + _rows.Clear(); + _searchResult.Clear(); + + var rows = new List(); + if (string.IsNullOrEmpty(file)) + { + MakeRows(rows, _tree, 0); + } + else + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null || vm.Commit == null) + return; + + var objects = vm.GetRevisionFilesUnderFolder(file); + if (objects == null || objects.Count != 1) + return; + + var routes = file.Split('/', StringSplitOptions.None); + if (routes.Length == 1) + { + _searchResult.Add(new ViewModels.RevisionFileTreeNode + { + Backend = objects[0] + }); + } + else + { + var last = _searchResult; + var prefix = string.Empty; + for (var i = 0; i < routes.Length - 1; i++) + { + var folder = new ViewModels.RevisionFileTreeNode + { + Backend = new Models.Object + { + Type = Models.ObjectType.Tree, + Path = prefix + routes[i], + }, + IsExpanded = true, + }; + + last.Add(folder); + last = folder.Children; + prefix = folder.Backend + "/"; + } + + last.Add(new ViewModels.RevisionFileTreeNode + { + Backend = objects[0] + }); + } + + MakeRows(rows, _searchResult, 0); + } + + _rows.AddRange(rows); + GC.Collect(); + } + public void ToggleNodeIsExpanded(ViewModels.RevisionFileTreeNode node) { _disableSelectionChangingEvent = true; @@ -189,6 +251,7 @@ namespace SourceGit.Views { _tree.Clear(); _rows.Clear(); + _searchResult.Clear(); var vm = DataContext as ViewModels.CommitDetail; if (vm == null || vm.Commit == null) @@ -308,5 +371,6 @@ namespace SourceGit.Views private List _tree = []; private AvaloniaList _rows = []; private bool _disableSelectionChangingEvent = false; + private List _searchResult = []; } } diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index d0b20963..fdb15807 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.RevisionFiles" x:DataType="vm:CommitDetail"> @@ -14,17 +15,96 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 53c36b1c..007c58ef 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -3,6 +3,7 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; @@ -118,5 +119,85 @@ namespace SourceGit.Views { InitializeComponent(); } + + private void OnSearchBoxKeyDown(object _, KeyEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + if (e.Key == Key.Enter) + { + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + e.Handled = true; + } + else if (e.Key == Key.Down || e.Key == Key.Up) + { + if (vm.IsRevisionFileSearchSuggestionOpen) + { + SearchSuggestionBox.Focus(NavigationMethod.Tab); + SearchSuggestionBox.SelectedIndex = 0; + } + + e.Handled = true; + } + else if (e.Key == Key.Escape) + { + if (vm.IsRevisionFileSearchSuggestionOpen) + { + vm.RevisionFileSearchSuggestion.Clear(); + vm.IsRevisionFileSearchSuggestionOpen = false; + } + + e.Handled = true; + } + } + + private void OnSearchBoxTextChanged(object _, TextChangedEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + if (string.IsNullOrEmpty(vm.RevisionFileSearchFilter)) + FileTree.SetSearchResult(null); + } + + private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + if (e.Key == Key.Escape) + { + vm.RevisionFileSearchSuggestion.Clear(); + e.Handled = true; + } + else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content) + { + vm.RevisionFileSearchFilter = content; + TxtSearchRevisionFiles.CaretIndex = content.Length; + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + e.Handled = true; + } + } + + private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e) + { + var vm = DataContext as ViewModels.CommitDetail; + if (vm == null) + return; + + var content = (sender as StackPanel)?.DataContext as string; + if (!string.IsNullOrEmpty(content)) + { + vm.RevisionFileSearchFilter = content; + TxtSearchRevisionFiles.CaretIndex = content.Length; + FileTree.SetSearchResult(vm.RevisionFileSearchFilter); + } + + e.Handled = true; + } } }