diff --git a/VSColorOutput.Tests/OutputClassifierProviderFilteringTests.cs b/VSColorOutput.Tests/OutputClassifierProviderFilteringTests.cs new file mode 100644 index 0000000..a1fbfcf --- /dev/null +++ b/VSColorOutput.Tests/OutputClassifierProviderFilteringTests.cs @@ -0,0 +1,82 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Utilities; +using Moq; +using System; +using System.Reflection; +using VSColorOutput.Output.ColorClassifier; + +namespace Tests +{ + [TestClass] + public class OutputClassifierProviderFilteringTests + { + private const string BufferFilterStateKey = "VSColorOutput.Output.BufferLineFilterState"; + + [TestInitialize] + public void Setup() + { + var field = typeof(OutputClassifierProvider).GetField("_outputClassifier", BindingFlags.Static | BindingFlags.NonPublic); + if (field != null) + { + field.SetValue(null, null); + } + } + + [TestMethod] + public void GetClassifier_AttachesHiddenLineFilterStateToOutputBuffer() + { + var provider = CreateProvider(); + + var properties = new PropertyCollection(); + var contentType = new Mock(); + contentType.Setup(c => c.IsOfType("Output")).Returns(true); + + var buffer = new Mock(); + buffer.SetupGet(b => b.Properties).Returns(properties); + buffer.SetupGet(b => b.ContentType).Returns(contentType.Object); + + var classifier = provider.GetClassifier(buffer.Object); + + classifier.Should().NotBeNull(); + properties.ContainsProperty(BufferFilterStateKey).Should().BeTrue(); + } + + [TestMethod] + public void GetClassifier_AttachesHiddenLineFilterOnlyOncePerBuffer() + { + var provider = CreateProvider(); + + var properties = new PropertyCollection(); + var contentType = new Mock(); + contentType.Setup(c => c.IsOfType("Output")).Returns(true); + + var changedSubscriptionCount = 0; + var buffer = new Mock(); + buffer.SetupGet(b => b.Properties).Returns(properties); + buffer.SetupGet(b => b.ContentType).Returns(contentType.Object); + buffer.SetupAdd(b => b.Changed += It.IsAny>()) + .Callback(() => changedSubscriptionCount++); + + provider.GetClassifier(buffer.Object); + provider.GetClassifier(buffer.Object); + + changedSubscriptionCount.Should().Be(1); + } + + private static OutputClassifierProvider CreateProvider() + { + var provider = new OutputClassifierProvider(); + + var registryField = typeof(OutputClassifierProvider).GetField("ClassificationRegistry", BindingFlags.Instance | BindingFlags.NonPublic); + registryField.SetValue(provider, new Mock().Object); + + var formatMapField = typeof(OutputClassifierProvider).GetField("ClassificationFormatMapService", BindingFlags.Instance | BindingFlags.NonPublic); + formatMapField.SetValue(provider, new Mock().Object); + + return provider; + } + } +} diff --git a/VSColorOutput.Tests/VSColorOutput.Tests.csproj b/VSColorOutput.Tests/VSColorOutput.Tests.csproj index 83dc643..be45950 100644 --- a/VSColorOutput.Tests/VSColorOutput.Tests.csproj +++ b/VSColorOutput.Tests/VSColorOutput.Tests.csproj @@ -57,6 +57,7 @@ + diff --git a/VSColorOutput/Output/ColorClassifier/OutputClassifierProvider.cs b/VSColorOutput/Output/ColorClassifier/OutputClassifierProvider.cs index cc85f19..841d697 100644 --- a/VSColorOutput/Output/ColorClassifier/OutputClassifierProvider.cs +++ b/VSColorOutput/Output/ColorClassifier/OutputClassifierProvider.cs @@ -2,7 +2,10 @@ using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Utilities; using System; +using System.Collections.Generic; using System.ComponentModel.Composition; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using VSColorOutput.State; @@ -15,6 +18,10 @@ namespace VSColorOutput.Output.ColorClassifier [Export(typeof(IClassifierProvider))] public class OutputClassifierProvider : IClassifierProvider { + private const string SuppliedClassifierForThisBufferKey = "VSColorOutput.Output.SuppliedClassifierForThisTextBufferKey"; + private const string BufferFilterStateKey = "VSColorOutput.Output.BufferLineFilterState"; + private static readonly object BufferFilterStateLock = new object(); + [Import] internal IClassificationTypeRegistryService ClassificationRegistry; @@ -22,7 +29,6 @@ public class OutputClassifierProvider : IClassifierProvider internal IClassificationFormatMapService ClassificationFormatMapService; private static OutputClassifier _outputClassifier; - private const string SuppliedClassifierForThisBufferKey = "VSColorOutput.Output.SuppliedClassifierForThisTextBufferKey"; public IClassifier GetClassifier(ITextBuffer buffer) { @@ -40,6 +46,7 @@ public IClassifier GetClassifier(ITextBuffer buffer) _outputClassifier.Initialize(ClassificationRegistry, ClassificationFormatMapService); } + AttachHiddenLineFilter(buffer); return _outputClassifier; } @@ -74,5 +81,90 @@ private static bool CanSupplyClassifier(ITextBuffer buffer) return false; } + + private static void AttachHiddenLineFilter(ITextBuffer buffer) + { + if (buffer.Properties.ContainsProperty(BufferFilterStateKey)) return; + + EventHandler handler = null; + handler = (sender, args) => HideMatchingLines(buffer, handler, args); + + lock (BufferFilterStateLock) + { + if (buffer.Properties.ContainsProperty(BufferFilterStateKey)) return; + buffer.Properties.AddProperty(BufferFilterStateKey, handler); + buffer.Changed += handler; + } + } + + private static void HideMatchingLines(ITextBuffer buffer, EventHandler handler, TextContentChangedEventArgs args) + { + if (args == null || args.After == null || args.EditTag == BufferFilterStateKey) return; + + try + { + var settings = Settings.Load(); + if (settings == null || string.IsNullOrWhiteSpace(settings.HiddenLinesRegExPattern)) return; + + var options = settings.HiddenLinesRegExIgnoreCase + ? RegexOptions.IgnoreCase + : RegexOptions.None; + + var regex = new Regex( + settings.HiddenLinesRegExPattern, + options, + TimeSpan.FromMilliseconds(250)); + + var spansToDelete = new List(); + foreach (var change in args.Changes) + { + var startLine = args.After.GetLineFromPosition(change.NewPosition).LineNumber; + var endPosition = change.NewEnd > change.NewPosition ? change.NewEnd - 1 : change.NewPosition; + var endLine = args.After.GetLineFromPosition(Math.Min(endPosition, Math.Max(args.After.Length - 1, 0))).LineNumber; + + for (var i = startLine; i <= endLine; i++) + { + var line = args.After.GetLineFromLineNumber(i); + var text = line.GetText(); + if (!regex.IsMatch(text)) continue; + + var span = line.LineBreakLength > 0 + ? line.ExtentIncludingLineBreak.Span + : line.Extent.Span; + + spansToDelete.Add(span); + } + } + + if (spansToDelete.Count == 0) return; + + using (var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, null, BufferFilterStateKey)) + { + foreach (var span in spansToDelete + .Distinct() + .OrderByDescending(s => s.Start)) + { + edit.Delete(span); + } + + if (edit.HasEffectiveChanges) + { + edit.Apply(); + } + } + } + catch (RegexMatchTimeoutException) + { + // eat it. + } + catch (ArgumentException) + { + // eat it. + } + catch (Exception ex) + { + Log.LogError(ex.ToString()); + } + } } -} \ No newline at end of file +} diff --git a/VSColorOutput/State/Settings.cs b/VSColorOutput/State/Settings.cs index 9d8b9ae..4237224 100644 --- a/VSColorOutput/State/Settings.cs +++ b/VSColorOutput/State/Settings.cs @@ -90,6 +90,12 @@ public class Settings [DataMember(Order = 21)] public string TimeStampDifference { get; set; } = DefaultTimeStampFormat; + [DataMember(Order = 23)] + public string HiddenLinesRegExPattern { get; set; } = string.Empty; + + [DataMember(Order = 24)] + public bool HiddenLinesRegExIgnoreCase { get; set; } = true; + private static readonly string ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VSColorOutput64"); public static event EventHandler SettingsUpdated; diff --git a/VSColorOutput/State/VsColorOutputOptionsDialog.cs b/VSColorOutput/State/VsColorOutputOptionsDialog.cs index 50a060b..7f90055 100644 --- a/VSColorOutput/State/VsColorOutputOptionsDialog.cs +++ b/VSColorOutput/State/VsColorOutputOptionsDialog.cs @@ -70,6 +70,15 @@ public class VsColorOutputOptionsDialog : DialogPage [Description("Formats elapsed build time, and elapsed and incremental times in debug output window according to system locale")] public bool FormatTimeInSystemLocale { get; set; } + [Category(ActionSubCategory)] + [DisplayName("Hide Output Lines (RegEx)")] + [Description("Any output line matching this regular expression will be removed from the Output window.")] + public string HiddenLinesRegExPattern { get; set; } + + [Category(ActionSubCategory)] + [DisplayName("Hide Output Lines Ignore Case")] + public bool HiddenLinesRegExIgnoreCase { get; set; } + [Category(ActionSubCategory)] [DisplayName("Yes, I Donated!")] [Description("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=REEP6X7DSPMZU")] @@ -162,6 +171,8 @@ public override void LoadSettingsFromStorage() ShowTimeStampOnEveryLine = settings.ShowTimeStampOnEveryLine; ShowHoursInTimeStamps = settings.ShowHoursInTimeStamps; FormatTimeInSystemLocale = settings.FormatTimeInSystemLocale; + HiddenLinesRegExPattern = settings.HiddenLinesRegExPattern; + HiddenLinesRegExIgnoreCase = settings.HiddenLinesRegExIgnoreCase; RegExPatterns = settings.Patterns; @@ -200,6 +211,8 @@ public override void SaveSettingsToStorage() ShowTimeStampOnEveryLine = ShowTimeStampOnEveryLine, ShowHoursInTimeStamps = ShowHoursInTimeStamps, FormatTimeInSystemLocale = FormatTimeInSystemLocale, + HiddenLinesRegExPattern = HiddenLinesRegExPattern, + HiddenLinesRegExIgnoreCase = HiddenLinesRegExIgnoreCase, // --- Patterns = RegExPatterns, // --- diff --git a/VSColorOutput/VSColorOutput.csproj b/VSColorOutput/VSColorOutput.csproj index 38f2d26..e5ba0a6 100644 --- a/VSColorOutput/VSColorOutput.csproj +++ b/VSColorOutput/VSColorOutput.csproj @@ -121,7 +121,9 @@ Always - + + +