diff options
author | Mike Krüger <mikkrg@microsoft.com> | 2018-04-11 17:53:06 +0300 |
---|---|---|
committer | Mike Krüger <mikkrg@microsoft.com> | 2018-04-11 17:53:06 +0300 |
commit | 5393a5a7cce3c4d9697aefd7951850eff601eda1 (patch) | |
tree | 2742ed6ea967410ce224e594b7a6269e54a2fb81 | |
parent | d2c212a04a47c3176572edcc85319459646f9e19 (diff) |
[SourceEditor] Extracted the span update listener logic & added unit
tests for it.
5 files changed, 282 insertions, 19 deletions
diff --git a/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.SpanUpdateListener.cs b/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.SpanUpdateListener.cs new file mode 100644 index 0000000000..f0700ee6ee --- /dev/null +++ b/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.SpanUpdateListener.cs @@ -0,0 +1,123 @@ +// +// TextViewMargin.SpanUpdateListener.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2018 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; + +using Mono.TextEditor.Highlighting; + +using MonoDevelop.Components.AtkCocoaHelper; + +using Gdk; +using Gtk; +using System.Timers; +using System.Diagnostics; +using MonoDevelop.Components; +using MonoDevelop.Core; +using MonoDevelop.Core.Text; +using MonoDevelop.Ide.Editor; +using MonoDevelop.Ide.Editor.Highlighting; +using System.Collections.Immutable; +using System.Threading; +using MonoDevelop.Ide; +using System.Threading.Tasks; + +namespace Mono.TextEditor +{ + partial class TextViewMargin + { + internal class SpanUpdateListener : IDisposable + { + readonly MonoTextEditor textEditor; + + public bool HasUpdatedMultilineSpan { get; set; } + + public SpanUpdateListener (MonoTextEditor textEditor) + { + this.textEditor = textEditor; + textEditor.Document.TextChanged += Document_TextChanged; + textEditor.Document.TextChanging += Document_TextChanging; + } + + public void Dispose() + { + textEditor.Document.TextChanged -= Document_TextChanged; + textEditor.Document.TextChanging -= Document_TextChanging; + } + + List<HighlightedLine> lines = new List<HighlightedLine> (); + + void Document_TextChanging (object sender, TextChangeEventArgs e) + { + HasUpdatedMultilineSpan = false; + foreach (var change in e.TextChanges) { + var layout = textEditor.TextViewMargin.GetLayout (textEditor.GetLineByOffset (change.Offset)); + lines.Add (layout.HighlightedLine); + } + } + + void Document_TextChanged (object sender, TextChangeEventArgs e) + { + int i = 0; + + foreach (var change in e.TextChanges) { + if (i >= lines.Count) + break; // should never happen + var oldHighlightedLine = lines [i++]; + var curLine = textEditor.GetLineByOffset (change.Offset); + var curLayout = textEditor.TextViewMargin.GetLayout (curLine); + if (!UpdateLineHighlight (curLine.LineNumber, oldHighlightedLine, curLayout.HighlightedLine)) + break; + } + lines.Clear (); + } + + bool UpdateLineHighlight (int lineNumber, HighlightedLine oldLine, HighlightedLine newLine) + { + if (oldLine != null && ShouldUpdateSpan (oldLine, newLine)) { + textEditor.TextViewMargin.PurgeLayoutCacheAfter (lineNumber); + textEditor.QueueDraw (); + HasUpdatedMultilineSpan = true; + return false; + } + return true; + } + + static bool ShouldUpdateSpan (HighlightedLine line1, HighlightedLine line2) + { + if (line1.IsContinuedBeyondLineEnd != line2.IsContinuedBeyondLineEnd) + return true; + if (line1.IsContinuedBeyondLineEnd == true) { + return line1.Segments.Last ().ScopeStack.Peek () != line2.Segments.Last ().ScopeStack.Peek (); + } + return false; + } + } + } +}
\ No newline at end of file diff --git a/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.cs b/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.cs index e6de21a47a..ea3ffd4c20 100644 --- a/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.cs +++ b/main/src/addins/MonoDevelop.SourceEditor2/Mono.TextEditor/Gui/TextViewMargin.cs @@ -35,7 +35,7 @@ using Mono.TextEditor.Highlighting; using MonoDevelop.Components.AtkCocoaHelper; -using Gdk; +using Gdk; using Gtk; using System.Timers; using System.Diagnostics; @@ -51,7 +51,7 @@ using System.Threading.Tasks; namespace Mono.TextEditor { - class TextViewMargin : Margin + partial class TextViewMargin : Margin { readonly MonoTextEditor textEditor; Pango.TabArray tabArray; @@ -62,6 +62,9 @@ namespace Mono.TextEditor internal double charWidth; bool isMonospacedFont; + SpanUpdateListener spanUpdateListener; + + internal SpanUpdateListener SpanUpdater { get => spanUpdateListener; } double LineHeight { get { @@ -290,7 +293,7 @@ namespace Mono.TextEditor accessible.Margin = this; this.textEditor = textEditor; - + spanUpdateListener = new SpanUpdateListener (textEditor); textEditor.Document.TextChanged += HandleTextReplaced; textEditor.HighlightSearchPatternChanged += TextEditor_HighlightSearchPatternChanged; textEditor.GetTextEditorData ().SearchChanged += HandleSearchChanged; @@ -716,7 +719,10 @@ namespace Mono.TextEditor marker.Dispose (); eolMarkerLayout = null; } - + if (spanUpdateListener != null) { + spanUpdateListener.Dispose (); + spanUpdateListener = null; + } DisposeLayoutDict (); if (tabArray != null) tabArray.Dispose (); @@ -1065,7 +1071,7 @@ namespace Mono.TextEditor continue; chunkMarker.TransformChunks (chunks); } - + wrapper.HighlightedLine = cachedChunks.Item3; wrapper.Chunks = chunks; foreach (var chunk in chunks) { try { @@ -1354,35 +1360,26 @@ namespace Mono.TextEditor } } CancellationTokenSource cacheSrc = new CancellationTokenSource (); - Tuple<List<ColoredSegment>, bool> GetCachedChunks (TextDocument doc, DocumentLine line, int offset, int length) + Tuple<List<ColoredSegment>, bool, HighlightedLine> GetCachedChunks (TextDocument doc, DocumentLine line, int offset, int length) { var lineNumber = line.LineNumber; var token = cacheSrc.Token; var task = doc.SyntaxMode.GetHighlightedLineAsync (line, token); if (task.IsCompleted) { if (task.Result != null) { - return Tuple.Create (TrimChunks (task.Result.Segments, offset - line.Offset, length), true); + return Tuple.Create (TrimChunks (task.Result.Segments, offset - line.Offset, length), true, task.Result); } } try { var taskResult = task.WaitAndGetResult (default (CancellationToken)); - return Tuple.Create (TrimChunks (taskResult.Segments, offset - line.Offset, length), true); + return Tuple.Create (TrimChunks (taskResult.Segments, offset - line.Offset, length), true, taskResult); } catch (Exception e) { LoggingService.LogError ("Error while highlighting", e); - return Tuple.Create (new List<ColoredSegment> (new [] { new ColoredSegment (0, line.Length, ScopeStack.Empty) }), false); - } - } - - static bool ShouldUpdateSpan (HighlightedLine line1, HighlightedLine line2) - { - if (line1.IsContinuedBeyondLineEnd != line2.IsContinuedBeyondLineEnd) - return true; - if (line1.IsContinuedBeyondLineEnd == true) { - return line1.Segments.Last ().ScopeStack.Peek () != line2.Segments.Last ().ScopeStack.Peek (); + return Tuple.Create (new List<ColoredSegment> (new [] { new ColoredSegment (0, line.Length, ScopeStack.Empty) }), false, (HighlightedLine)null); } - return false; } + internal static List<ColoredSegment> TrimChunks (IReadOnlyList<ColoredSegment> segments, int offset, int length) { var result = new List<ColoredSegment> (); @@ -1536,6 +1533,8 @@ namespace Mono.TextEditor set; } + internal HighlightedLine HighlightedLine { get; set; } + public string Text { get { return Layout.Text; diff --git a/main/src/addins/MonoDevelop.SourceEditor2/MonoDevelop.SourceEditor.csproj b/main/src/addins/MonoDevelop.SourceEditor2/MonoDevelop.SourceEditor.csproj index 4a411f46ec..17e95a1ff6 100644 --- a/main/src/addins/MonoDevelop.SourceEditor2/MonoDevelop.SourceEditor.csproj +++ b/main/src/addins/MonoDevelop.SourceEditor2/MonoDevelop.SourceEditor.csproj @@ -286,6 +286,7 @@ <Compile Include="MonoDevelop.SourceEditor\TextMarker\LineSeparatorMarker.cs" /> <Compile Include="MonoDevelop.SourceEditor.QuickTasks\QuickTaskOverviewMode.IndicatorDrawingCache.cs" /> <Compile Include="MonoDevelop.SourceEditor.QuickTasks\QuickTaskOverviewMode.IdleUpdater.cs" /> + <Compile Include="Mono.TextEditor\Gui\TextViewMargin.SpanUpdateListener.cs" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="gtk-gui\gui.stetic"> diff --git a/main/src/core/MonoDevelop.TextEditor.Tests/Mono.TextEditor.Tests/TextViewTests.cs b/main/src/core/MonoDevelop.TextEditor.Tests/Mono.TextEditor.Tests/TextViewTests.cs new file mode 100644 index 0000000000..8c953d9c54 --- /dev/null +++ b/main/src/core/MonoDevelop.TextEditor.Tests/Mono.TextEditor.Tests/TextViewTests.cs @@ -0,0 +1,139 @@ +// +// TextViewTests.cs +// +// Author: +// Mike Krüger <mikkrg@microsoft.com> +// +// Copyright (c) 2018 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.IO; +using System.Text; +using System.Linq; +using NUnit.Framework; +using Mono.TextEditor.Utils; +using System.Reflection; +using MonoDevelop.Core.Text; +using MonoDevelop.Ide.Editor.Highlighting; +using MonoDevelop.Ide.Editor; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Mono.TextEditor.Tests +{ + [TestFixture] + public class TextViewTests + { + class TestSyntaxMode : ISyntaxHighlighting + { + public event EventHandler<MonoDevelop.Ide.Editor.LineEventArgs> HighlightingStateChanged; + + public void Dispose () + { + } + + public Task<HighlightedLine> GetHighlightedLineAsync (IDocumentLine line, CancellationToken cancellationToken) + { + var segments = new List<ColoredSegment> (); + for (int i = 0; i < line.Length;i++){ + if (i > 0 && i == line.Length - 1) { + segments.Add (new ColoredSegment (i, 2, ScopeStack.Empty)); + break; + } + segments.Add (new ColoredSegment (i, 1, ScopeStack.Empty)); + } + + return Task.FromResult (new HighlightedLine (line, segments)); + } + + public Task<ScopeStack> GetScopeStackAsync (int offset, CancellationToken cancellationToken) + { + return Task.FromResult (ScopeStack.Empty); + } + } + + [Test] + public void TestFirstTimeUpdate () + { + var editor = new MonoTextEditor (); + editor.GetTextEditorData ().Document.SyntaxMode = new TestSyntaxMode (); + editor.Text = @"1 +2 +3 +4 +5"; + editor.Caret.Location = new DocumentLocation (2, 1); + editor.InsertAtCaret ("a"); + Assert.IsTrue (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + } + + [Test] + public void TestSubsequentUpdate () + { + var editor = new MonoTextEditor (); + editor.GetTextEditorData ().Document.SyntaxMode = new TestSyntaxMode (); + editor.Text = @"1 +2 +3 +4 +5"; + editor.Caret.Location = new DocumentLocation (2, 1); + editor.InsertAtCaret ("a"); + Assert.IsTrue (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + editor.InsertAtCaret ("a"); + Assert.IsFalse (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + } + + [Test] + public void TestBackspace () + { + var editor = new MonoTextEditor (); + editor.GetTextEditorData ().Document.SyntaxMode = new TestSyntaxMode (); + editor.Text = @"1 +2 +3 +4 +5"; + editor.Caret.Location = new DocumentLocation (2, 1); + editor.InsertAtCaret ("a"); + Assert.IsTrue (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + DeleteActions.Backspace (editor.GetTextEditorData ()); + Assert.IsTrue (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + } + + [Test] + public void TestDeleteNoUpdate () + { + var editor = new MonoTextEditor (); + editor.GetTextEditorData ().Document.SyntaxMode = new TestSyntaxMode (); + editor.Text = @"1 +2 +3 +4 +5"; + editor.Caret.Location = new DocumentLocation (2, 1); + DeleteActions.Delete (editor.GetTextEditorData ()); + Assert.IsFalse (editor.TextViewMargin.SpanUpdater.HasUpdatedMultilineSpan); + } + + } +}
\ No newline at end of file diff --git a/main/src/core/MonoDevelop.TextEditor.Tests/MonoDevelop.TextEditor.Tests.csproj b/main/src/core/MonoDevelop.TextEditor.Tests/MonoDevelop.TextEditor.Tests.csproj index 2e81e3c00d..aff4067d1a 100644 --- a/main/src/core/MonoDevelop.TextEditor.Tests/MonoDevelop.TextEditor.Tests.csproj +++ b/main/src/core/MonoDevelop.TextEditor.Tests/MonoDevelop.TextEditor.Tests.csproj @@ -81,6 +81,7 @@ <Compile Include="Mono.TextEditor.Tests\TextEditorDataTests.cs" /> <Compile Include="MonoDevelop.TextEditor.Extension\NavigationExtensionTests.cs" /> <Compile Include="Mono.TextEditor.Tests\DiffTrackerTests.cs" /> + <Compile Include="Mono.TextEditor.Tests\TextViewTests.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <ItemGroup> |