diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs index ba01c884a6..ed7cd42a6b 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs @@ -267,7 +267,7 @@ namespace ICSharpCode.AvalonEdit.Document /// /// Gets the previous segment before the specified segment. /// Segments are sorted by their start offset. - /// Returns null if segment is the last segment. + /// Returns null if segment is the first segment. /// public T GetPreviousSegment(T segment) { @@ -277,6 +277,26 @@ namespace ICSharpCode.AvalonEdit.Document } #endregion + #region FirstSegment/LastSegment + /// + /// Returns the first segment in the collection or null, if the collection is empty. + /// + public T FirstSegment { + get { + return root == null ? null : (T)root.LeftMost; + } + } + + /// + /// Returns the last segment in the collection or null, if the collection is empty. + /// + public T LastSegment { + get { + return root == null ? null : (T)root.RightMost; + } + } + #endregion + #region FindFirstSegmentWithStartAfter /// /// Gets the first segment with a start offset greater or equal to . diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs index 8e1c2f5505..300b3e50c9 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs @@ -12,9 +12,9 @@ using System.Text; using System.Windows; using System.Windows.Documents; using System.Windows.Input; - using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.AvalonEdit.Search; using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Editing diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs index 04ba96bf70..e7bb039932 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs @@ -2,8 +2,10 @@ // This code is distributed under the GNU LGPL (for details please see \doc\license.txt) using System; +using System.Linq; using System.Windows; using System.Windows.Input; + using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Search; @@ -41,6 +43,8 @@ namespace ICSharpCode.AvalonEdit.Editing this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, ExecuteUndo, CanExecuteUndo)); this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, ExecuteRedo, CanExecuteRedo)); this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, ExecuteFind)); + this.CommandBindings.Add(new CommandBinding(SearchCommands.FindNext, ExecuteFindNext)); + this.CommandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, ExecuteFindPrevious)); } internal static KeyBinding CreateFrozenKeyBinding(ICommand command, ModifierKeys modifiers, Key key) @@ -109,7 +113,25 @@ namespace ICSharpCode.AvalonEdit.Editing void ExecuteFind(object sender, ExecutedRoutedEventArgs e) { - new SearchPanel(TextArea); + var panel = TextArea.TextView.Layers.OfType().FirstOrDefault(); + if (panel == null) + new SearchPanel(TextArea); + else + panel.Reactivate(); + } + + void ExecuteFindNext(object sender, ExecutedRoutedEventArgs e) + { + var panel = TextArea.TextView.Layers.OfType().FirstOrDefault(); + if (panel != null) + panel.FindNext(); + } + + void ExecuteFindPrevious(object sender, ExecutedRoutedEventArgs e) + { + var panel = TextArea.TextView.Layers.OfType().FirstOrDefault(); + if (panel != null) + panel.FindPrevious(); } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 59c9586d27..3d529d71b3 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -290,7 +290,11 @@ VisualLine.cs - + + + + + SearchPanel.xaml Code @@ -412,6 +416,9 @@ + + DropDownButton.cs + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DefaultSearchStrategy.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DefaultSearchStrategy.cs new file mode 100644 index 0000000000..47a8d68ba9 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DefaultSearchStrategy.cs @@ -0,0 +1,52 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Folding; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Search +{ + class DefaultSearchStrategy : ISearchStrategy + { + readonly Regex searchPattern; + + public DefaultSearchStrategy(Regex searchPattern) + { + this.searchPattern = searchPattern; + } + + public static ISearchStrategy Create(string searchPattern, bool ignoreCase, bool useRegularExpressions, bool matchWholeWords) + { + RegexOptions options = RegexOptions.Compiled; + if (ignoreCase) + options |= RegexOptions.IgnoreCase; + if (!useRegularExpressions) + searchPattern = Regex.Escape(searchPattern); + if (matchWholeWords) + searchPattern = "\\b" + searchPattern + "\\b"; + Regex pattern = new Regex(searchPattern, options); + return new DefaultSearchStrategy(pattern); + } + + public IEnumerable FindAll(ITextSource document) + { + foreach (Match result in searchPattern.Matches(document.Text)) { + yield return new SearchResult { StartOffset = result.Index, Length = result.Length }; + } + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.cs new file mode 100644 index 0000000000..af2aac883c --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.cs @@ -0,0 +1,60 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +namespace ICSharpCode.AvalonEdit.Search +{ + /// + /// A button that opens a drop-down menu when clicked. + /// + class DropDownButton : ButtonBase + { + public static readonly DependencyProperty DropDownContentProperty + = DependencyProperty.Register("DropDownContent", typeof(Popup), + typeof(DropDownButton), new FrameworkPropertyMetadata(null)); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")] + protected static readonly DependencyPropertyKey IsDropDownContentOpenPropertyKey + = DependencyProperty.RegisterReadOnly("IsDropDownContentOpen", typeof(bool), + typeof(DropDownButton), new FrameworkPropertyMetadata(false)); + + public static readonly DependencyProperty IsDropDownContentOpenProperty = IsDropDownContentOpenPropertyKey.DependencyProperty; + + static DropDownButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(DropDownButton), new FrameworkPropertyMetadata(typeof(DropDownButton))); + } + + public Popup DropDownContent { + get { return (Popup)GetValue(DropDownContentProperty); } + set { SetValue(DropDownContentProperty, value); } + } + + public bool IsDropDownContentOpen { + get { return (bool)GetValue(IsDropDownContentOpenProperty); } + protected set { SetValue(IsDropDownContentOpenPropertyKey, value); } + } + + protected override void OnClick() + { + if (DropDownContent != null && !IsDropDownContentOpen) { + DropDownContent.Placement = PlacementMode.Bottom; + DropDownContent.PlacementTarget = this; + DropDownContent.IsOpen = true; + DropDownContent.Closed += DropDownContent_Closed; + this.IsDropDownContentOpen = true; + } + } + + void DropDownContent_Closed(object sender, EventArgs e) + { + ((Popup)sender).Closed -= DropDownContent_Closed; + this.IsDropDownContentOpen = false; + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.xaml b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.xaml new file mode 100644 index 0000000000..597f2fba4c --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/DropDownButton.xaml @@ -0,0 +1,71 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/ISearchStrategy.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/ISearchStrategy.cs new file mode 100644 index 0000000000..0dc2722dec --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/ISearchStrategy.cs @@ -0,0 +1,22 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using ICSharpCode.AvalonEdit.Document; + +namespace ICSharpCode.AvalonEdit.Search +{ + /// + /// Description of ISearchStrategy. + /// + public interface ISearchStrategy + { + IEnumerable FindAll(ITextSource document); + } + + public interface ISearchResult : ISegment + { + + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchCommands.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchCommands.cs new file mode 100644 index 0000000000..1f8f12979e --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchCommands.cs @@ -0,0 +1,32 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Search +{ + public static class SearchCommands + { + public static readonly RoutedCommand FindNext = new RoutedCommand( + "FindNext", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.F3) } + ); + public static readonly RoutedCommand FindPrevious = new RoutedCommand( + "FindPrevious", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.F3, ModifierKeys.Control) } + ); + } + + class SearchResult : TextSegment, ISearchResult + { + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml index ada9117e83..d12ae27f99 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml @@ -1,24 +1,37 @@  - + - + + + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml.cs index 65aba681c3..24131474af 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; @@ -13,6 +15,7 @@ using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Folding; using ICSharpCode.AvalonEdit.Rendering; namespace ICSharpCode.AvalonEdit.Search @@ -25,6 +28,11 @@ namespace ICSharpCode.AvalonEdit.Search TextArea textArea; SearchResultBackgroundRenderer renderer; SearchResult currentResult; + FoldingManager foldingManager; + + public bool UseRegex { get { return useRegex.IsChecked == true; } } + public bool MatchCase { get { return matchCase.IsChecked == true; } } + public bool WholeWords { get { return wholeWords.IsChecked == true; } } public SearchPanel(TextArea textArea) { @@ -35,32 +43,77 @@ namespace ICSharpCode.AvalonEdit.Search InitializeComponent(); textArea.TextView.Layers.Add(this); + foldingManager = textArea.GetService(typeof(FoldingManager)) as FoldingManager; renderer = new SearchResultBackgroundRenderer(); textArea.TextView.BackgroundRenderers.Add(renderer); textArea.Document.TextChanged += delegate { DoSearch(false); }; + this.Loaded += delegate { searchTextBox.Focus(); }; + + useRegex.Checked += delegate { DoSearch(true); }; + matchCase.Checked += delegate { DoSearch(true); }; + wholeWords.Checked += delegate { DoSearch(true); }; - Dispatcher.Invoke(DispatcherPriority.Input, (Action)(() => searchTextBox.Focus())); + useRegex.Unchecked += delegate { DoSearch(true); }; + matchCase.Unchecked += delegate { DoSearch(true); }; + wholeWords.Unchecked += delegate { DoSearch(true); }; } void SearchTextBoxTextChanged(object sender, TextChangedEventArgs e) { DoSearch(true); } + + /// + /// Reactivates the SearchPanel by setting the focus on the search box and selecting all text. + /// + public void Reactivate() + { + searchTextBox.Focus(); + searchTextBox.SelectAll(); + } + + public void FindNext() + { + SearchResult result = null; + if (currentResult != null) + result = renderer.CurrentResults.GetNextSegment(currentResult); + if (result == null) + result = renderer.CurrentResults.FirstSegment; + if (result != null) { + currentResult = result; + SetResult(result); + } + } + + public void FindPrevious() + { + SearchResult result = null; + if (currentResult != null) + result = renderer.CurrentResults.GetPreviousSegment(currentResult); + if (result == null) + result = renderer.CurrentResults.LastSegment; + if (result != null) { + currentResult = result; + SetResult(result); + } + } void DoSearch(bool changeSelection) { renderer.CurrentResults.Clear(); if (!string.IsNullOrEmpty(searchTextBox.Text)) { - + ISearchStrategy strategy = DefaultSearchStrategy.Create(searchTextBox.Text, !MatchCase, UseRegex, WholeWords); currentResult = null; int offset = textArea.Caret.Offset; - foreach (var result in FindAll(searchTextBox.Text, textArea.Document)) { + if (changeSelection) { + textArea.Selection = SimpleSelection.Empty; + } + foreach (SearchResult result in strategy.FindAll(textArea.Document)) { if (currentResult == null && result.StartOffset >= offset) { currentResult = result; if (changeSelection) { - textArea.Caret.Offset = currentResult.StartOffset; - textArea.Selection = new SimpleSelection(currentResult.StartOffset, currentResult.EndOffset); + SetResult(result); } } renderer.CurrentResults.Add(result); @@ -68,32 +121,17 @@ namespace ICSharpCode.AvalonEdit.Search } textArea.TextView.InvalidateLayer(KnownLayer.Selection); } - - IEnumerable FindAll(string search, TextDocument document) + + void SetResult(SearchResult result) { - SearchResult lastResult = FindNext(search, 0, document); - while (lastResult != null) { - yield return lastResult; - lastResult = FindNext(search, lastResult.StartOffset + lastResult.Length, document); + textArea.Caret.Offset = currentResult.StartOffset; + textArea.Selection = new SimpleSelection(currentResult.StartOffset, currentResult.EndOffset); + if (foldingManager != null) { + foreach (var folding in foldingManager.GetFoldingsContaining(result.StartOffset)) + folding.IsFolded = false; } } - SearchResult FindNext(string search, int index, TextDocument document) - { - int result = document.Text.IndexOf(search, index, StringComparison.OrdinalIgnoreCase); - if (result > -1) - return new SearchResult { StartOffset = result, Length = search.Length }; - return null; - } - - SearchResult FindPrev(string search, int index, TextDocument document) - { - int result = document.GetText(0, index).LastIndexOf(search, StringComparison.OrdinalIgnoreCase); - if (result > -1) - return new SearchResult { StartOffset = result, Length = search.Length }; - return null; - } - void SearchLayerKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) { @@ -106,27 +144,5 @@ namespace ICSharpCode.AvalonEdit.Search textArea.TextView.Layers.Remove(this); textArea.TextView.BackgroundRenderers.Remove(renderer); } - - void PrevClick(object sender, RoutedEventArgs e) - { - if (currentResult != null) { - var result = FindPrev(searchTextBox.Text, currentResult.StartOffset, textArea.Document); - if (result != null) { - currentResult = result; - textArea.Selection = new SimpleSelection(currentResult.StartOffset, currentResult.EndOffset); - } - } - } - - void NextClick(object sender, RoutedEventArgs e) - { - if (currentResult != null) { - var result = FindNext(searchTextBox.Text, currentResult.EndOffset, textArea.Document); - if (result != null) { - currentResult = result; - textArea.Selection = new SimpleSelection(currentResult.StartOffset, currentResult.EndOffset); - } - } - } } } \ No newline at end of file diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchResultBackgroundRenderer.cs similarity index 97% rename from src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.cs rename to src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchResultBackgroundRenderer.cs index d1d45f68ed..9605b06846 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchPanel.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Search/SearchResultBackgroundRenderer.cs @@ -14,10 +14,6 @@ using ICSharpCode.AvalonEdit.Rendering; namespace ICSharpCode.AvalonEdit.Search { - class SearchResult : TextSegment - { - } - class SearchResultBackgroundRenderer : IBackgroundRenderer { TextSegmentCollection currentResults = new TextSegmentCollection(); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/themes/generic.xaml b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/themes/generic.xaml index ad76a3eea0..f14ce0e98a 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/themes/generic.xaml +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/themes/generic.xaml @@ -1,9 +1,9 @@ + xmlns:editing="clr-namespace:ICSharpCode.AvalonEdit.Editing"> +