diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs index 0b20c8b4..1051126b 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,16 @@ namespace ICSharpCode.AvalonEdit.Search /// public class SearchPanel : Control { + /// + /// Specifies the time to wait till the entered text is searched for + /// + 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; @@ -44,6 +55,10 @@ public class SearchPanel : Control Popup dropdownPopup; SearchPanelAdorner adorner; + DispatcherTimer typingTimer; + bool lastChangeSelection; + + #region DependencyProperties /// /// Dependency property for . @@ -55,7 +70,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 +86,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 +102,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 +118,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 +134,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); } } @@ -181,7 +201,8 @@ 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); } } @@ -197,7 +218,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(); } @@ -291,7 +313,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); } @@ -318,13 +341,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); } @@ -349,8 +375,10 @@ public void FindNext() SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + 1); if (result == null) result = renderer.CurrentResults.FirstSegment; - if (result != null) { - SelectResult(result); + if (result != null) + { + var s = Selection.Create(textArea, result.StartOffset, result.EndOffset); + SelectResult(result, textArea.Caret.Line >= s.StartPosition.Line); } } @@ -364,8 +392,10 @@ public void FindPrevious() result = renderer.CurrentResults.GetPreviousSegment(result); if (result == null) result = renderer.CurrentResults.LastSegment; - if (result != null) { - SelectResult(result); + if (result != null) + { + var s = Selection.Create(textArea, result.StartOffset, result.EndOffset); + SelectResult(result, textArea.Caret.Line <= s.StartPosition.Line); } } @@ -375,52 +405,90 @@ 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) { - SelectResult(result); + foreach (SearchResult result in strategy.FindAll(textArea.Document, 0, textArea.Document.TextLength)) + { + if (changeSelection && result.StartOffset >= offset) + { + SelectResult(result, false); 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) + 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) { - 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; @@ -485,7 +553,8 @@ public void Open() /// protected virtual void OnSearchOptionsChanged(SearchOptionsChangedEventArgs e) { - if (SearchOptionsChanged != null) { + if (SearchOptionsChanged != null) + { SearchOptionsChanged(this, e); } } @@ -539,7 +608,8 @@ public SearchPanelAdorner(TextArea textArea, SearchPanel panel) AddVisualChild(panel); } - protected override int VisualChildrenCount { + protected override int VisualChildrenCount + { get { return 1; } }