diff --git a/.gitattributes b/.gitattributes index fbfb41be..1ff0c423 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,63 @@ -*.cs text diff=csharp -*.sln text eol=crlf -*.csproj text eol=crlf +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/ICSharpCode.AvalonEdit.Sample/Window1.xaml.cs b/ICSharpCode.AvalonEdit.Sample/Window1.xaml.cs index 0fba481a..ccdbe81e 100644 --- a/ICSharpCode.AvalonEdit.Sample/Window1.xaml.cs +++ b/ICSharpCode.AvalonEdit.Sample/Window1.xaml.cs @@ -75,6 +75,8 @@ public Window1() foldingUpdateTimer.Interval = TimeSpan.FromSeconds(2); foldingUpdateTimer.Tick += delegate { UpdateFoldings(); }; foldingUpdateTimer.Start(); + + Search.SearchPanel.Install(textEditor); } string currentFileName; diff --git a/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj b/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj index 774bc93d..b0cad20c 100644 --- a/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj +++ b/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj @@ -1,7 +1,7 @@  Library - net6.0-windows;netcoreapp3.1;net462 + net6.0-windows;netcoreapp3.1;net472 true true TRACE diff --git a/ICSharpCode.AvalonEdit/.gitignore b/ICSharpCode.AvalonEdit/.gitignore new file mode 100644 index 00000000..e3eab68c --- /dev/null +++ b/ICSharpCode.AvalonEdit/.gitignore @@ -0,0 +1 @@ +/ICSharpCode.AvalonEdit.xml diff --git a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index f7ceb98e..365bc364 100644 --- a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -1,7 +1,7 @@  Library - net6.0-windows;netcoreapp3.1;net462 + net6.0-windows;netcoreapp3.1;net472 true true TRACE @@ -66,4 +66,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/ICSharpCode.AvalonEdit/Search/Localization.cs b/ICSharpCode.AvalonEdit/Search/Localization.cs index d7876c39..dc62c3eb 100644 --- a/ICSharpCode.AvalonEdit/Search/Localization.cs +++ b/ICSharpCode.AvalonEdit/Search/Localization.cs @@ -59,6 +59,20 @@ public virtual string FindPreviousText { get { return "Find previous (Shift+F3)"; } } + /// + /// Default: 'Replace next (ALT+R)' + /// + public virtual string ReplaceNextText { + get { return "Replace next (ALT+R)"; } + } + + /// + /// Default: 'Replace all (ALT+A)' + /// + public virtual string ReplaceAllText { + get { return "Replace all (ALT+A)"; } + } + /// /// Default: 'Error: ' /// diff --git a/ICSharpCode.AvalonEdit/Search/SearchCommands.cs b/ICSharpCode.AvalonEdit/Search/SearchCommands.cs index 1fbc23f0..c6ab9abf 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchCommands.cs +++ b/ICSharpCode.AvalonEdit/Search/SearchCommands.cs @@ -30,6 +30,22 @@ namespace ICSharpCode.AvalonEdit.Search /// public static class SearchCommands { + /// + /// Opens the Find panel + /// + public static readonly RoutedCommand Find = new RoutedCommand( + "Find", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.F, ModifierKeys.Control) } + ); + + /// + /// Opens the Replace panel + /// + public static readonly RoutedCommand Replace = new RoutedCommand( + "Replace", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.H, ModifierKeys.Control) } + ); + /// /// Finds the next occurrence in the file. /// @@ -46,6 +62,22 @@ public static class SearchCommands new InputGestureCollection { new KeyGesture(Key.F3, ModifierKeys.Shift) } ); + /// + /// Replaces the current occurrence and finds the next occurrence in the file. + /// + public static readonly RoutedCommand ReplaceNext = new RoutedCommand( + "ReplaceNext", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.R, ModifierKeys.Alt) } + ); + + /// + /// Replaces all occurrence in the file. + /// + public static readonly RoutedCommand ReplaceAll = new RoutedCommand( + "ReplaceAll", typeof(SearchPanel), + new InputGestureCollection { new KeyGesture(Key.A, ModifierKeys.Alt) } + ); + /// /// Closes the SearchPanel. /// @@ -70,15 +102,25 @@ internal SearchInputHandler(TextArea textArea, SearchPanel panel) internal void RegisterGlobalCommands(CommandBindingCollection commandBindings) { commandBindings.Add(new CommandBinding(ApplicationCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(ApplicationCommands.Replace, ExecuteReplace)); + commandBindings.Add(new CommandBinding(SearchCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(SearchCommands.Replace, ExecuteReplace)); commandBindings.Add(new CommandBinding(SearchCommands.FindNext, ExecuteFindNext, CanExecuteWithOpenSearchPanel)); commandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, ExecuteFindPrevious, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.ReplaceNext, ExecuteReplaceNext, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.ReplaceAll, ExecuteReplaceAll, CanExecuteWithOpenSearchPanel)); } void RegisterCommands(ICollection commandBindings) { commandBindings.Add(new CommandBinding(ApplicationCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(ApplicationCommands.Replace, ExecuteReplace)); + commandBindings.Add(new CommandBinding(SearchCommands.Find, ExecuteFind)); + commandBindings.Add(new CommandBinding(SearchCommands.Replace, ExecuteReplace)); commandBindings.Add(new CommandBinding(SearchCommands.FindNext, ExecuteFindNext, CanExecuteWithOpenSearchPanel)); commandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, ExecuteFindPrevious, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.ReplaceNext, ExecuteReplaceNext, CanExecuteWithOpenSearchPanel)); + commandBindings.Add(new CommandBinding(SearchCommands.ReplaceAll, ExecuteReplaceAll, CanExecuteWithOpenSearchPanel)); commandBindings.Add(new CommandBinding(SearchCommands.CloseSearchPanel, ExecuteCloseSearchPanel, CanExecuteWithOpenSearchPanel)); } @@ -86,7 +128,14 @@ void RegisterCommands(ICollection commandBindings) void ExecuteFind(object sender, ExecutedRoutedEventArgs e) { - panel.Open(); + panel.Open(false); + if (!(TextArea.Selection.IsEmpty || TextArea.Selection.IsMultiline)) + panel.SearchPattern = TextArea.Selection.GetText(); + Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, (Action)delegate { panel.Reactivate(); }); + } + + void ExecuteReplace(object sender, ExecutedRoutedEventArgs e) { + panel.Open(true); if (!(TextArea.Selection.IsEmpty || TextArea.Selection.IsMultiline)) panel.SearchPattern = TextArea.Selection.GetText(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, (Action)delegate { panel.Reactivate(); }); @@ -119,6 +168,20 @@ void ExecuteFindPrevious(object sender, ExecutedRoutedEventArgs e) e.Handled = true; } } + + void ExecuteReplaceNext(object sender, ExecutedRoutedEventArgs e) { + if (!panel.IsClosed) { + panel.ReplaceNext(); + e.Handled = true; + } + } + + void ExecuteReplaceAll(object sender, ExecutedRoutedEventArgs e) { + if (!panel.IsClosed) { + panel.ReplaceAll(); + e.Handled = true; + } + } void ExecuteCloseSearchPanel(object sender, ExecutedRoutedEventArgs e) { diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs index 0b20c8b4..01f61a98 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchPanel.cs +++ b/ICSharpCode.AvalonEdit/Search/SearchPanel.cs @@ -41,6 +41,8 @@ public class SearchPanel : Control TextDocument currentDocument; SearchResultBackgroundRenderer renderer; TextBox searchTextBox; + TextBox replaceTextBox; + Border searchPanel; Popup dropdownPopup; SearchPanelAdorner adorner; @@ -105,6 +107,36 @@ public string SearchPattern { set { SetValue(SearchPatternProperty, value); } } + /// + /// Dependency property for . + /// + public static readonly DependencyProperty ReplacementProperty = + DependencyProperty.Register("Replacement", typeof(string), typeof(SearchPanel), + new FrameworkPropertyMetadata("")); + + /// + /// Gets/sets the replacement. + /// + public string Replacement { + get { return (string)GetValue(ReplacementProperty); } + set { SetValue(ReplacementProperty, value); } + } + + /// + /// Dependency property for . + /// + public static readonly DependencyProperty ShowReplaceProperty = + DependencyProperty.Register("ShowReplace", typeof(bool), typeof(SearchPanel), + new FrameworkPropertyMetadata(false)); + + /// + /// Gets/sets whether the replace is shown. + /// + public bool ShowReplace { + get { return (bool)GetValue(ShowReplaceProperty); } + set { SetValue(ShowReplaceProperty, value); } + } + /// /// Dependency property for . /// @@ -280,8 +312,12 @@ void AttachInternal(TextArea textArea) textArea.DocumentChanged += textArea_DocumentChanged; KeyDown += SearchLayerKeyDown; + this.CommandBindings.Add(new CommandBinding(SearchCommands.Find, (sender, e) => Open(false))); + this.CommandBindings.Add(new CommandBinding(SearchCommands.Replace, (sender, e) => Open(true))); this.CommandBindings.Add(new CommandBinding(SearchCommands.FindNext, (sender, e) => FindNext())); this.CommandBindings.Add(new CommandBinding(SearchCommands.FindPrevious, (sender, e) => FindPrevious())); + this.CommandBindings.Add(new CommandBinding(SearchCommands.ReplaceNext, (sender, e) => ReplaceNext())); + this.CommandBindings.Add(new CommandBinding(SearchCommands.ReplaceAll, (sender, e) => ReplaceAll())); this.CommandBindings.Add(new CommandBinding(SearchCommands.CloseSearchPanel, (sender, e) => Close())); IsClosed = true; } @@ -307,7 +343,9 @@ public override void OnApplyTemplate() { base.OnApplyTemplate(); + searchPanel = Template.FindName("PART_searchPanel", this) as Border; searchTextBox = Template.FindName("PART_searchTextBox", this) as TextBox; + replaceTextBox = Template.FindName("PART_replaceTextBox", this) as TextBox; dropdownPopup = Template.FindName("PART_dropdownPopup", this) as Popup; } @@ -369,6 +407,42 @@ public void FindPrevious() } } + /// + /// Replaces current result if any and moves to the next occurrence in the file. + /// + public int ReplaceNext() { + SearchResult result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset); + var count = renderer.CurrentResults.Count; + if (result != null + && !textArea.Selection.IsEmpty + && textArea.Document.GetOffset(textArea.Selection.StartPosition.Location) == result.StartOffset + && textArea.Document.GetOffset(textArea.Selection.EndPosition.Location) == result.EndOffset) { + Replace(result); + --count; + } + result = renderer.CurrentResults.FindFirstSegmentWithStartAfter(textArea.Caret.Offset + textArea.Selection.Length); + if (result == null) + result = renderer.CurrentResults.FirstSegment; + if (result != null) { + SelectResult(result); + return count; + } + return 0; + } + + /// + /// Replaces all occurrences in the file. + /// + public void ReplaceAll() { + var count = ReplaceNext(); + while (count-- > 0) + ReplaceNext(); + } + + void Replace(SearchResult result) { + currentDocument.Replace(textArea.Selection.Segments.FirstOrDefault(), result.ReplaceWith(Replacement)); + } + ToolTip messageView = new ToolTip { Placement = PlacementMode.Bottom, StaysOpen = true, Focusable = false }; void DoSearch(bool changeSelection) @@ -393,7 +467,7 @@ void DoSearch(bool changeSelection) if (!renderer.CurrentResults.Any()) { messageView.IsOpen = true; messageView.Content = Localization.NoMatchesFoundText; - messageView.PlacementTarget = searchTextBox; + messageView.PlacementTarget = searchPanel; } else messageView.IsOpen = false; } @@ -414,16 +488,21 @@ void SearchLayerKeyDown(object sender, KeyEventArgs e) switch (e.Key) { case Key.Enter: e.Handled = true; - if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) - FindPrevious(); - else - FindNext(); - if (searchTextBox != null) { - var error = Validation.GetErrors(searchTextBox).FirstOrDefault(); - if (error != null) { - messageView.Content = Localization.ErrorText + " " + error.ErrorContent; - messageView.PlacementTarget = searchTextBox; - messageView.IsOpen = true; + if (replaceTextBox != null + && replaceTextBox.IsFocused) { + ReplaceNext(); + } else { + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) + FindPrevious(); + else + FindNext(); + if (searchTextBox != null) { + var error = Validation.GetErrors(searchTextBox).FirstOrDefault(); + if (error != null) { + messageView.Content = Localization.ErrorText + " " + error.ErrorContent; + messageView.PlacementTarget = searchPanel; + messageView.IsOpen = true; + } } } break; @@ -464,8 +543,9 @@ public void Close() /// /// Opens the an existing search panel. /// - public void Open() + public void Open(bool showReplace) { + ShowReplace = showReplace; if (!IsClosed) return; var layer = AdornerLayer.GetAdornerLayer(textArea); if (layer != null) diff --git a/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml b/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml index 4fd0ec34..daac003f 100644 --- a/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml +++ b/ICSharpCode.AvalonEdit/Search/SearchPanel.xaml @@ -5,40 +5,62 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ICSharpCode.AvalonEdit/Search/replaceall.png b/ICSharpCode.AvalonEdit/Search/replaceall.png new file mode 100644 index 00000000..9d4446f1 Binary files /dev/null and b/ICSharpCode.AvalonEdit/Search/replaceall.png differ diff --git a/ICSharpCode.AvalonEdit/Search/replacenext.png b/ICSharpCode.AvalonEdit/Search/replacenext.png new file mode 100644 index 00000000..a5852696 Binary files /dev/null and b/ICSharpCode.AvalonEdit/Search/replacenext.png differ