diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs b/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs index e97df5b613..f7e6cd8086 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs @@ -28,7 +28,7 @@ namespace SearchAndReplace /// public class SearchManager { - static IEnumerable GenerateFileList(SearchTarget target, string baseDirectory = null, string filter = "*.*", bool searchSubdirs = false) + static IEnumerable GenerateFileList(SearchTarget target, string baseDirectory = null, string filter = "*.*", bool searchSubdirs = false, CancellationToken ct = default(CancellationToken)) { List files = new List(); @@ -40,28 +40,39 @@ namespace SearchAndReplace files.Add(vc.TextEditor.FileName); break; case SearchTarget.AllOpenFiles: - foreach (ITextEditorProvider editor in WorkbenchSingleton.Workbench.ViewContentCollection.OfType()) + foreach (ITextEditorProvider editor in WorkbenchSingleton.Workbench.ViewContentCollection.OfType()) { + if (ct != default(CancellationToken)) + ct.ThrowIfCancellationRequested(); files.Add(editor.TextEditor.FileName); + } break; case SearchTarget.WholeProject: if (ProjectService.CurrentProject == null) break; - foreach (FileProjectItem item in ProjectService.CurrentProject.Items.OfType()) + foreach (FileProjectItem item in ProjectService.CurrentProject.Items.OfType()) { + if (ct != default(CancellationToken)) + ct.ThrowIfCancellationRequested(); files.Add(new FileName(item.FileName)); + } break; case SearchTarget.WholeSolution: if (ProjectService.OpenSolution == null) break; - foreach (var item in ProjectService.OpenSolution.SolutionFolderContainers.Select(f => f.SolutionItems).SelectMany(si => si.Items)) + foreach (var item in ProjectService.OpenSolution.SolutionFolderContainers.Select(f => f.SolutionItems).SelectMany(si => si.Items)) { + if (ct != default(CancellationToken)) + ct.ThrowIfCancellationRequested(); files.Add(new FileName(Path.Combine(ProjectService.OpenSolution.Directory, item.Location))); - foreach (var item in ProjectService.OpenSolution.Projects.SelectMany(p => p.Items).OfType()) + } + foreach (var item in ProjectService.OpenSolution.Projects.SelectMany(p => p.Items).OfType()) { + if (ct != default(CancellationToken)) + ct.ThrowIfCancellationRequested(); files.Add(new FileName(item.FileName)); + } break; case SearchTarget.Directory: if (!Directory.Exists(baseDirectory)) break; - foreach (var name in FileUtility.SearchDirectory(baseDirectory, filter, searchSubdirs)) - files.Add(new FileName(name)); + return EnumerateDirectories(baseDirectory, filter, searchSubdirs, ct); break; default: throw new Exception("Invalid value for FileListType"); @@ -69,20 +80,60 @@ namespace SearchAndReplace return files.Distinct(); } + + static IEnumerable EnumerateDirectories(string baseDirectory, string filter, bool searchSubdirs) + { + foreach (var name in FileUtility.SearchDirectory(baseDirectory, filter, searchSubdirs)) { + yield return name; + } + } + + public static void ShowSearchResults(string pattern, IObservable results) + { + string title = StringParser.Parse("${res:MainWindow.Windows.SearchResultPanel.OccurrencesOf}", + new StringTagPair("Pattern", pattern)); + SearchResultsPad.Instance.ShowSearchResults(title, results); + SearchResultsPad.Instance.BringToFront(); + } + public static IObservable FindAll(string pattern, bool ignoreCase, bool matchWholeWords, SearchMode mode, SearchTarget target, string baseDirectory = null, string filter = "*.*", bool searchSubdirs = false) { CancellationTokenSource cts = new CancellationTokenSource(); var monitor = WorkbenchSingleton.Workbench.StatusBar.CreateProgressMonitor(cts.Token); - monitor.TaskName = "Find All"; + monitor.TaskName = "Find all occurrences of '" + pattern + "' in " + GetTargetDescription(target, baseDirectory); monitor.Status = OperationStatus.Normal; var strategy = SearchStrategyFactory.Create(pattern, ignoreCase, matchWholeWords, mode); ParseableFileContentFinder fileFinder = new ParseableFileContentFinder(); - var fileList = GenerateFileList(target, baseDirectory, filter, searchSubdirs); + IEnumerable fileList; + using (IProgressMonitor dialog = AsynchronousWaitDialog.ShowWaitDialog("Prepare search ...", true)) { + dialog.Progress = double.NaN; + fileList = GenerateFileList(target, baseDirectory, filter, searchSubdirs, dialog.CancellationToken); + } return new SearchRun(strategy, fileFinder, fileList, monitor, cts); } + static string GetTargetDescription(SearchTarget target, string baseDirectory = null) + { + switch (target) { + case SearchTarget.CurrentDocument: + return "the current document"; + case SearchTarget.CurrentSelection: + return "the current selection"; + case SearchTarget.AllOpenFiles: + return "all open files"; + case SearchTarget.WholeProject: + return "the whole project"; + case SearchTarget.WholeSolution: + return "the whole solution"; + case SearchTarget.Directory: + return "the directory '" + baseDirectory + "'"; + default: + throw new Exception("Invalid value for SearchTarget"); + } + } + class SearchRun : IObservable, IDisposable { IObserver observer; @@ -104,7 +155,9 @@ namespace SearchAndReplace public IDisposable Subscribe(IObserver observer) { this.observer = observer; - new System.Threading.Tasks.Task(delegate { Parallel.ForEach(fileList, fileName => SearchFile(fileFinder.Create(fileName).CreateSnapshot(), strategy, monitor.CancellationToken)); }).Start(); + var task = new System.Threading.Tasks.Task(delegate { Parallel.ForEach(fileList, new ParallelOptions { CancellationToken = monitor.CancellationToken, MaxDegreeOfParallelism = Environment.ProcessorCount }, fileName => SearchFile(fileName, strategy, monitor.CancellationToken)); }); + task.ContinueWith(t => { if (t.Exception != null) observer.OnError(t.Exception); else observer.OnCompleted(); this.Dispose(); }); + task.Start(); return this; } @@ -115,17 +168,23 @@ namespace SearchAndReplace monitor.Dispose(); } - void SearchFile(ITextBuffer buffer, ISearchStrategy strategy, CancellationToken ct) + void SearchFile(FileName fileName, ISearchStrategy strategy, CancellationToken ct) { + ITextBuffer buffer = fileFinder.Create(fileName); + if (buffer == null) + return; + buffer = buffer.CreateSnapshot(); + if (!MimeTypeDetection.FindMimeType(buffer).StartsWith("text/")) return; var source = DocumentUtilitites.GetTextSource(buffer); foreach(var result in strategy.FindAll(source)) { ct.ThrowIfCancellationRequested(); - observer.OnNext(new SearchResultMatch(result.Offset, result.Length)); + lock (observer) + observer.OnNext(new SearchResultMatch(new ProvidedDocumentInformation(buffer, fileName, 0), result.Offset, result.Length)); } lock (monitor) - monitor.Progress += 1 / fileList.Count(); + monitor.Progress += 1.0 / fileList.Count(); } } } diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Gui/DefaultSearchResult.cs b/src/AddIns/Misc/SearchAndReplace/Project/Gui/DefaultSearchResult.cs index a268ff7bc5..05832104f8 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Gui/DefaultSearchResult.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Gui/DefaultSearchResult.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; - +using System.Windows.Threading; using ICSharpCode.Core; using ICSharpCode.Core.Presentation; using ICSharpCode.SharpDevelop; @@ -130,15 +130,13 @@ namespace SearchAndReplace public class ObserverSearchResult : ISearchResult, IObserver { - List matches; - Button stopButton; + static Button stopButton; SearchRootNode rootNode; public ObserverSearchResult(string title) { Text = title; - matches = new List(); - rootNode = new SearchRootNode(title, matches); + rootNode = new SearchRootNode(title, new List()); } public string Text { get; private set; } @@ -156,17 +154,76 @@ namespace SearchAndReplace return resultsTreeViewInstance; } + static IList toolbarItems; + static MenuItem flatItem, perFileItem; + public IList GetToolbarItems() { - stopButton = new Button { Content = "Stop" }; - stopButton.Click += delegate { if (Registration != null) Registration.Dispose(); }; - - return new ArrayList { stopButton }; + WorkbenchSingleton.AssertMainThread(); + if (toolbarItems == null) { + toolbarItems = new List(); + DropDownButton perFileDropDown = new DropDownButton(); + perFileDropDown.Content = new Image { Height = 16, Source = PresentationResourceService.GetBitmapSource("Icons.16x16.FindIcon") }; + perFileDropDown.SetValueToExtension(DropDownButton.ToolTipProperty, new LocalizeExtension("MainWindow.Windows.SearchResultPanel.SelectViewMode.ToolTip")); + + flatItem = new MenuItem(); + flatItem.SetValueToExtension(MenuItem.HeaderProperty, new LocalizeExtension("MainWindow.Windows.SearchResultPanel.Flat")); + flatItem.Click += delegate { SetPerFile(false); }; + + perFileItem = new MenuItem(); + perFileItem.SetValueToExtension(MenuItem.HeaderProperty, new LocalizeExtension("MainWindow.Windows.SearchResultPanel.PerFile")); + perFileItem.Click += delegate { SetPerFile(true); }; + + perFileDropDown.DropDownMenu = new ContextMenu(); + perFileDropDown.DropDownMenu.Items.Add(flatItem); + perFileDropDown.DropDownMenu.Items.Add(perFileItem); + toolbarItems.Add(perFileDropDown); + toolbarItems.Add(new Separator()); + + Button expandAll = new Button(); + expandAll.SetValueToExtension(Button.ToolTipProperty, new LocalizeExtension("MainWindow.Windows.SearchResultPanel.ExpandAll.ToolTip")); + expandAll.Content = new Image { Height = 16, Source = PresentationResourceService.GetBitmapSource("Icons.16x16.OpenAssembly") }; + expandAll.Click += delegate { ExpandCollapseAll(true); }; + toolbarItems.Add(expandAll); + + Button collapseAll = new Button(); + collapseAll.SetValueToExtension(Button.ToolTipProperty, new LocalizeExtension("MainWindow.Windows.SearchResultPanel.CollapseAll.ToolTip")); + collapseAll.Content = new Image { Height = 16, Source = PresentationResourceService.GetBitmapSource("Icons.16x16.Assembly") }; + collapseAll.Click += delegate { ExpandCollapseAll(false); }; + toolbarItems.Add(collapseAll); + + stopButton = new Button { Content = "Stop" }; + stopButton.Click += delegate { + stopButton.Visibility = Visibility.Collapsed; + if (Registration != null) Registration.Dispose(); + }; + toolbarItems.Add(stopButton); + } + return toolbarItems; + } + + static void ExpandCollapseAll(bool newIsExpanded) + { + if (resultsTreeViewInstance != null) { + foreach (SearchNode node in resultsTreeViewInstance.ItemsSource.OfType().Flatten(n => n.Children)) { + node.IsExpanded = newIsExpanded; + } + } + } + + static void SetPerFile(bool perFile) + { + ResultsTreeView.GroupResultsByFile = perFile; + if (resultsTreeViewInstance != null) { + foreach (SearchRootNode node in resultsTreeViewInstance.ItemsSource.OfType()) { + node.GroupResultsByFile(perFile); + } + } } void IObserver.OnNext(SearchResultMatch value) { - matches.Add(value); + WorkbenchSingleton.SafeThreadCall((Action)delegate { rootNode.Add(value); }); } void IObserver.OnError(Exception error) @@ -177,7 +234,12 @@ namespace SearchAndReplace void OnCompleted() { - stopButton.Visibility = Visibility.Collapsed; + WorkbenchSingleton.SafeThreadCall( + (Action)delegate { + stopButton.Visibility = Visibility.Collapsed; + if (Registration != null) + Registration.Dispose(); + }); } void IObserver.OnCompleted() diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchAndReplacePanel.cs b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchAndReplacePanel.cs index c87c6d9d96..80edb96522 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchAndReplacePanel.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchAndReplacePanel.cs @@ -132,7 +132,7 @@ namespace SearchAndReplace } } else { var results = SearchManager.FindAll(SearchOptions.FindPattern, !SearchOptions.MatchCase, SearchOptions.MatchWholeWord, SearchOptions.SearchStrategyType, SearchOptions.DocumentIteratorType, SearchOptions.LookIn, SearchOptions.LookInFiletypes, SearchOptions.IncludeSubdirectories); - SearchResultsPad.Instance.ShowSearchResults("Search", results); + SearchManager.ShowSearchResults(SearchOptions.FindPattern, results); } } diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchFileNode.cs b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchFileNode.cs index a4da1443c9..9957ae012f 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchFileNode.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchFileNode.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Windows.Controls; using System.Windows.Documents; + using ICSharpCode.Core; using ICSharpCode.SharpDevelop; using ICSharpCode.SharpDevelop.Editor.Search; @@ -13,11 +14,12 @@ namespace SearchAndReplace { class SearchFileNode : SearchNode { - FileName fileName; + public FileName FileName { get; private set; } + - public SearchFileNode(FileName fileName, SearchResultNode[] resultNodes) + public SearchFileNode(FileName fileName, System.Collections.Generic.List resultNodes) { - this.fileName = fileName; + this.FileName = fileName; this.Children = resultNodes; this.IsExpanded = true; } @@ -26,15 +28,15 @@ namespace SearchAndReplace { return new TextBlock { Inlines = { - new Bold(new Run(Path.GetFileName(fileName))), - new Run(StringParser.Parse(" (${res:MainWindow.Windows.SearchResultPanel.In} ") + Path.GetDirectoryName(fileName) + ")") + new Bold(new Run(Path.GetFileName(FileName))), + new Run(StringParser.Parse(" (${res:MainWindow.Windows.SearchResultPanel.In} ") + Path.GetDirectoryName(FileName) + ")") } }; } public override void ActivateItem() { - FileService.OpenFile(fileName); + FileService.OpenFile(FileName); } } } diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchNode.cs b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchNode.cs index 62a7479144..0cad40daa6 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchNode.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchNode.cs @@ -58,7 +58,7 @@ namespace SearchAndReplace protected abstract object CreateText(); - protected void InvalidateText() + protected internal void InvalidateText() { cachedText = null; OnPropertyChanged("Text"); diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchRootNode.cs b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchRootNode.cs index 0a3db6b5cf..f598d5d6ab 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchRootNode.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Gui/SearchRootNode.cs @@ -3,13 +3,13 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; - using ICSharpCode.Core; using ICSharpCode.SharpDevelop; using ICSharpCode.SharpDevelop.Editor.Search; @@ -18,20 +18,32 @@ namespace SearchAndReplace { sealed class SearchRootNode : SearchNode { - IList resultNodes; - IList fileNodes; + ObservableCollection resultNodes; + ObservableCollection fileNodes; public string Title { get; private set; } public SearchRootNode(string title, IList results) { this.Title = title; - this.resultNodes = results.Select(r => new SearchResultNode(r)).ToArray(); - this.fileNodes = resultNodes.GroupBy(r => r.FileName).Select(g => new SearchFileNode(g.Key, g.ToArray())).ToArray(); - - this.Children = this.resultNodes; + this.resultNodes = new ObservableCollection(results.Select(r => new SearchResultNode(r))); + this.fileNodes = new ObservableCollection(resultNodes.GroupBy(r => r.FileName).Select(g => new SearchFileNode(g.Key, g.ToList()))); this.IsExpanded = true; } + + public void Add(SearchResultMatch match) + { + var matchNode = new SearchResultNode(match); + resultNodes.Add(matchNode); + int index = fileNodes.FindIndex(node => node.FileName.Equals(match.FileName)); + if (index == -1) + fileNodes.Add(new SearchFileNode(match.FileName, new List { matchNode })); + else { + fileNodes[index].Children = resultNodes.Where(node => node.FileName.Equals(match.FileName)).ToArray(); + fileNodes[index].InvalidateText(); + } + InvalidateText(); + } public void GroupResultsByFile(bool perFile) { diff --git a/src/AddIns/Misc/SearchAndReplace/Project/SearchAndReplace.addin b/src/AddIns/Misc/SearchAndReplace/Project/SearchAndReplace.addin index a3aa411e64..b36b5c6963 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/SearchAndReplace.addin +++ b/src/AddIns/Misc/SearchAndReplace/Project/SearchAndReplace.addin @@ -29,8 +29,8 @@ shortcut = "Control|F3" class = "SearchAndReplace.FindNextSelected"/> +