From 82afb0d0d5cfcfa8c0e286d0b659be454b48fd76 Mon Sep 17 00:00:00 2001 From: carsten_franke Date: Wed, 14 Oct 2020 14:31:30 +0200 Subject: [PATCH 1/3] Added configurable delay before executing the search which improves user experience with large documents --- ICSharpCode.AvalonEdit/Search/SearchPanel.cs | 106 ++++++++++++++----- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs index d8e37c07..540c085b 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs +++ b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs @@ -24,6 +24,7 @@ using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; +using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; @@ -36,6 +37,8 @@ namespace ICSharpCode.AvalonEdit.Search /// public class SearchPanel : Control { + public static readonly int DelayBeforeSearch = 250; + TextArea textArea; SearchInputHandler handler; TextDocument currentDocument; @@ -44,6 +47,10 @@ public class SearchPanel : Control Popup dropdownPopup; SearchPanelAdorner adorner; + DispatcherTimer typingTimer; + bool lastChangeSelection; + + #region DependencyProperties /// /// Dependency property for . @@ -55,7 +62,8 @@ public class SearchPanel : Control /// /// Gets/sets whether the search pattern should be interpreted as regular expression. /// - public bool UseRegex { + public bool UseRegex + { get { return (bool)GetValue(UseRegexProperty); } set { SetValue(UseRegexProperty, value); } } @@ -70,7 +78,8 @@ public bool UseRegex { /// /// Gets/sets whether the search pattern should be interpreted case-sensitive. /// - public bool MatchCase { + public bool MatchCase + { get { return (bool)GetValue(MatchCaseProperty); } set { SetValue(MatchCaseProperty, value); } } @@ -85,7 +94,8 @@ public bool MatchCase { /// /// Gets/sets whether the search pattern should only match whole words. /// - public bool WholeWords { + public bool WholeWords + { get { return (bool)GetValue(WholeWordsProperty); } set { SetValue(WholeWordsProperty, value); } } @@ -100,7 +110,8 @@ public bool WholeWords { /// /// Gets/sets the search pattern. /// - public string SearchPattern { + public string SearchPattern + { get { return (string)GetValue(SearchPatternProperty); } set { SetValue(SearchPatternProperty, value); } } @@ -115,7 +126,8 @@ public string SearchPattern { /// /// Gets/sets the Brush used for marking search results in the TextView. /// - public Brush MarkerBrush { + public Brush MarkerBrush + { get { return (Brush)GetValue(MarkerBrushProperty); } set { SetValue(MarkerBrushProperty, value); } } @@ -130,7 +142,8 @@ public Brush MarkerBrush { /// /// Gets/sets the localization for the SearchPanel. /// - public Localization Localization { + public Localization Localization + { get { return (Localization)GetValue(LocalizationProperty); } set { SetValue(LocalizationProperty, value); } } @@ -139,7 +152,8 @@ public Localization Localization { static void MarkerBrushChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { SearchPanel panel = d as SearchPanel; - if (panel != null) { + if (panel != null) + { panel.renderer.MarkerBrush = (Brush)e.NewValue; } } @@ -154,7 +168,8 @@ static SearchPanel() static void SearchPatternChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { SearchPanel panel = d as SearchPanel; - if (panel != null) { + if (panel != null) + { panel.ValidateSearchText(); panel.UpdateSearch(); } @@ -248,7 +263,8 @@ void textArea_DocumentChanged(object sender, EventArgs e) if (currentDocument != null) currentDocument.TextChanged -= textArea_Document_TextChanged; currentDocument = textArea.Document; - if (currentDocument != null) { + if (currentDocument != null) + { currentDocument.TextChanged += textArea_Document_TextChanged; DoSearch(false); } @@ -275,13 +291,16 @@ void ValidateSearchText() var be = searchTextBox.GetBindingExpression(TextBox.TextProperty); - try { + try + { if (be != null) Validation.ClearInvalid(be); UpdateSearch(); - } catch (SearchPatternException ex) { + } + catch (SearchPatternException ex) + { var ve = new ValidationError(be.ParentBinding.ValidationRules[0], be, ex.Message, ex); Validation.MarkInvalid(be, ve); } @@ -306,7 +325,8 @@ public void FindNext() SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + 1); if (result == null) result = renderer.CurrentResults.FirstSegment; - if (result != null) { + if (result != null) + { SelectResult(result); } } @@ -321,7 +341,8 @@ public void FindPrevious() result = renderer.CurrentResults.GetPreviousSegment(result); if (result == null) result = renderer.CurrentResults.LastSegment; - if (result != null) { + if (result != null) + { SelectResult(result); } } @@ -332,29 +353,61 @@ void DoSearch(bool changeSelection) { if (IsClosed) return; + + lastChangeSelection = changeSelection; + if (typingTimer == null) + { + typingTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(DelayBeforeSearch) + }; + + typingTimer.Tick += handleTypingTimerTimeout; + } + typingTimer.Stop(); + typingTimer.Start(); + } + + private void handleTypingTimerTimeout(object sender, EventArgs e) + { + var timer = sender as DispatcherTimer; // WPF + if (timer == null) + { + return; + } + + var changeSelection = lastChangeSelection; renderer.CurrentResults.Clear(); - if (!string.IsNullOrEmpty(SearchPattern)) { + if (!string.IsNullOrEmpty(SearchPattern)) + { int offset = textArea.Caret.Offset; - if (changeSelection) { + if (changeSelection) + { textArea.ClearSelection(); } // We cast from ISearchResult to SearchResult; this is safe because we always use the built-in strategy - foreach (SearchResult result in strategy.FindAll(textArea.Document, 0, textArea.Document.TextLength)) { - if (changeSelection && result.StartOffset >= offset) { + foreach (SearchResult result in strategy.FindAll(textArea.Document, 0, textArea.Document.TextLength)) + { + if (changeSelection && result.StartOffset >= offset) + { SelectResult(result); changeSelection = false; } renderer.CurrentResults.Add(result); } - if (!renderer.CurrentResults.Any()) { + if (!renderer.CurrentResults.Any()) + { messageView.IsOpen = true; messageView.Content = Localization.NoMatchesFoundText; messageView.PlacementTarget = searchTextBox; - } else + } + else messageView.IsOpen = false; } textArea.TextView.InvalidateLayer(KnownLayer.Selection); + + timer.Stop(); } void SelectResult(SearchResult result) @@ -368,16 +421,19 @@ void SelectResult(SearchResult result) void SearchLayerKeyDown(object sender, KeyEventArgs e) { - switch (e.Key) { + switch (e.Key) + { case Key.Enter: e.Handled = true; if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) FindPrevious(); else FindNext(); - if (searchTextBox != null) { + if (searchTextBox != null) + { var error = Validation.GetErrors(searchTextBox).FirstOrDefault(); - if (error != null) { + if (error != null) + { messageView.Content = Localization.ErrorText + " " + error.ErrorContent; messageView.PlacementTarget = searchTextBox; messageView.IsOpen = true; @@ -442,7 +498,8 @@ public void Open() /// protected virtual void OnSearchOptionsChanged(SearchOptionsChangedEventArgs e) { - if (SearchOptionsChanged != null) { + if (SearchOptionsChanged != null) + { SearchOptionsChanged(this, e); } } @@ -496,7 +553,8 @@ public SearchPanelAdorner(TextArea textArea, SearchPanel panel) AddVisualChild(panel); } - protected override int VisualChildrenCount { + protected override int VisualChildrenCount + { get { return 1; } } From a1c92990c5c540b75cb9127943281de6bb44ce92 Mon Sep 17 00:00:00 2001 From: Carsten Franke Date: Fri, 29 Sep 2023 10:40:27 +0200 Subject: [PATCH 2/3] Added missing XML documentation --- ICSharpCode.AvalonEdit/Search/SearchPanel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs index c6cc54fe..51bce88e 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs +++ b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs @@ -37,7 +37,10 @@ namespace ICSharpCode.AvalonEdit.Search /// public class SearchPanel : Control { - public static readonly int DelayBeforeSearch = 250; + /// + /// Specifies the time to wait till the entered text is searched for + /// + public static int DelayBeforeSearch { get; set; } = 250; TextArea textArea; SearchInputHandler handler; From 51e377fc6c888fb91453202228cf0bfd8323d2fe Mon Sep 17 00:00:00 2001 From: Carsten Franke Date: Fri, 29 Nov 2024 08:50:09 +0100 Subject: [PATCH 3/3] Added event which triggers when the search wraps the end/start of the file --- ICSharpCode.AvalonEdit/Search/SearchPanel.cs | 34 +++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs index 51bce88e..1051126b 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs +++ b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs @@ -42,6 +42,11 @@ public class SearchPanel : Control /// public static int DelayBeforeSearch { get; set; } = 250; + /// + /// Event that occurs if the search wrapped the end or beginning of the file + /// + public static event EventHandler SearchWrapped; + TextArea textArea; SearchInputHandler handler; TextDocument currentDocument; @@ -97,7 +102,7 @@ public bool MatchCase /// /// Gets/sets whether the search pattern should only match whole words. /// - public bool WholeWords + public bool WholeWords { get { return (bool)GetValue(WholeWordsProperty); } set { SetValue(WholeWordsProperty, value); } @@ -196,7 +201,7 @@ private static void MarkerCornerRadiusChangedCallback(DependencyObject d, Depend /// /// Gets/sets the localization for the SearchPanel. /// - public Localization Localization + public Localization Localization { get { return (Localization)GetValue(LocalizationProperty); } set { SetValue(LocalizationProperty, value); } @@ -370,9 +375,10 @@ public void FindNext() SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + 1); if (result == null) result = renderer.CurrentResults.FirstSegment; - if (result != null) + if (result != null) { - SelectResult(result); + var s = Selection.Create(textArea, result.StartOffset, result.EndOffset); + SelectResult(result, textArea.Caret.Line >= s.StartPosition.Line); } } @@ -388,7 +394,8 @@ public void FindPrevious() result = renderer.CurrentResults.LastSegment; if (result != null) { - SelectResult(result); + var s = Selection.Create(textArea, result.StartOffset, result.EndOffset); + SelectResult(result, textArea.Caret.Line <= s.StartPosition.Line); } } @@ -400,7 +407,7 @@ void DoSearch(bool changeSelection) return; lastChangeSelection = changeSelection; - if (typingTimer == null) + if (typingTimer == null) { typingTimer = new DispatcherTimer { @@ -416,7 +423,7 @@ void DoSearch(bool changeSelection) private void handleTypingTimerTimeout(object sender, EventArgs e) { var timer = sender as DispatcherTimer; // WPF - if (timer == null) + if (timer == null) { return; } @@ -427,7 +434,7 @@ private void handleTypingTimerTimeout(object sender, EventArgs e) if (!string.IsNullOrEmpty(SearchPattern)) { int offset = textArea.Caret.Offset; - if (changeSelection) + if (changeSelection) { textArea.ClearSelection(); } @@ -436,7 +443,7 @@ private void handleTypingTimerTimeout(object sender, EventArgs e) { if (changeSelection && result.StartOffset >= offset) { - SelectResult(result); + SelectResult(result, false); changeSelection = false; } renderer.CurrentResults.Add(result); @@ -446,7 +453,7 @@ private void handleTypingTimerTimeout(object sender, EventArgs e) messageView.IsOpen = true; messageView.Content = Localization.NoMatchesFoundText; messageView.PlacementTarget = searchTextBox; - } + } else messageView.IsOpen = false; } @@ -455,13 +462,16 @@ private void handleTypingTimerTimeout(object sender, EventArgs e) timer.Stop(); } - void SelectResult(SearchResult result) + void SelectResult(SearchResult result, bool searchWrapped) { textArea.Caret.Offset = result.StartOffset; textArea.Selection = Selection.Create(textArea, result.StartOffset, result.EndOffset); textArea.Caret.BringCaretToView(); // show caret even if the editor does not have the Keyboard Focus textArea.Caret.Show(); + if (searchWrapped) { + SearchWrapped?.Invoke(this, EventArgs.Empty); + } } void SearchLayerKeyDown(object sender, KeyEventArgs e) @@ -474,7 +484,7 @@ void SearchLayerKeyDown(object sender, KeyEventArgs e) FindPrevious(); else FindNext(); - if (searchTextBox != null) + if (searchTextBox != null) { var error = Validation.GetErrors(searchTextBox).FirstOrDefault(); if (error != null)