diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs index e98cd8b964..62eab02257 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs @@ -7,23 +7,19 @@ using System; using System.Collections.ObjectModel; -using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; -using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; -using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.AvalonEdit.AddIn.Options; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; using ICSharpCode.AvalonEdit.Highlighting; -using ICSharpCode.AvalonEdit.Indentation; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.Core; using ICSharpCode.Core.Presentation; @@ -33,7 +29,6 @@ using ICSharpCode.SharpDevelop.Dom; using ICSharpCode.SharpDevelop.Editor; using ICSharpCode.SharpDevelop.Editor.AvalonEdit; using ICSharpCode.SharpDevelop.Editor.CodeCompletion; -using ICSharpCode.SharpDevelop.Editor.Commands; namespace ICSharpCode.AvalonEdit.AddIn { diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs index 9140ac8fb1..c131d49962 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs @@ -60,7 +60,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting { CheckIsHighlighting(); int number = line.LineNumber; - InvalidateHighlighting(); storedSpanStacks.RemoveAt(number); isValid.RemoveAt(number); if (number < isValid.Count) { diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs index e37c2bc197..870535a262 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs @@ -80,6 +80,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting { base.OnAddToTextView(textView); textView.DocumentChanged += textView_DocumentChanged; + textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting; OnDocumentChanged(textView); } @@ -90,6 +91,19 @@ namespace ICSharpCode.AvalonEdit.Highlighting textView.Services.RemoveService(typeof(IHighlighter)); textView.Services.RemoveService(typeof(DocumentHighlighter)); textView.DocumentChanged -= textView_DocumentChanged; + textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting; + } + + void textView_VisualLineConstructionStarting(object sender, VisualLineConstructionStartEventArgs e) + { + IHighlighter highlighter = ((TextView)sender).Services.GetService(typeof(IHighlighter)) as IHighlighter; + if (highlighter != null) { + // Force update of highlighting state up to the position where we start generating visual lines. + // This is necessary in case the document gets modified above the FirstLineInView so that the highlighting state changes. + // We need to detect this case and issue a redraw (through TextViewDocumentHighligher.OnHighlightStateChanged) + // before the visual line construction reuses existing lines that were built using the invalid highlighting state. + highlighter.GetSpanStack(e.FirstLineInView.LineNumber - 1); + } } /// @@ -128,7 +142,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting sealed class TextViewDocumentHighlighter : DocumentHighlighter { - TextView textView; + readonly TextView textView; public TextViewDocumentHighlighter(TextView textView, TextDocument document, HighlightingRuleSet baseRuleSet) : base(document, baseRuleSet) @@ -140,18 +154,58 @@ namespace ICSharpCode.AvalonEdit.Highlighting protected override void OnHighlightStateChanged(DocumentLine line, int lineNumber) { base.OnHighlightStateChanged(line, lineNumber); - int lineEndOffset = line.Offset + line.TotalLength; - if (lineEndOffset >= 0) { - // Do not use colorizer.CurrentContext - the colorizer might not be the only - // class calling DocumentHighlighter.HighlightLine, the the context might be null. - int length = this.Document.TextLength - lineEndOffset; - if (length != 0) { - // don't redraw if length == 0: at the end of the document, this would cause - // the last line which was already constructed to be redrawn -> - // we would get an exception due to disposing the line that was already constructed - textView.Redraw(lineEndOffset, length, DispatcherPriority.Normal); - } + if (textView.Document != this.Document) { + // May happen if document on text view was changed but some user code is still using the + // existing IHighlighter instance. + return; } + + // The user may have inserted "/*" into the current line, and so far only that line got redrawn. + // So when the highlighting state is changed, we issue a redraw for the line immediately below. + // If the highlighting state change applies to the lines below, too, the construction of each line + // will invalidate the next line, and the construction pass will regenerate all lines. + + Debug.WriteLine("OnHighlightStateChanged forces redraw of line " + (lineNumber + 1)); + + // If the VisualLine construction is in progress, we have to avoid sending redraw commands for + // anything above the line currently being constructed. + // It takes some explanation to see why this cannot happen. + // VisualLines always get constructed from top to bottom. + // Each VisualLine construction calls into the highlighter and thus forces an update of the + // highlighting state for all lines up to the one being constructed. + + // To guarantee that we don't redraw lines we just constructed, we need to show that when + // a VisualLine is being reused, the highlighting state at that location is still up-to-date. + + // This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes + // (e.g. split view). + + // For the first line in the view, the TextView.VisualLineConstructionStarting event is used to check that the + // highlighting state is up-to-date. If it isn't, this method will be executed, and it'll mark the first line + // in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused. + + // Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom + // construction process. + + // We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1. + + // Start of induction: the first line in view can is reused only if the highlighting state was up-to-date + // until line N-1 (no change detected in VisualLineConstructionStarting event). + + // Induction step: + // If another line N+1 is being reused, then either + // a) the previous line (the visual line containing document line N) was newly constructed + // or b) the previous line was reused + // In case a, the construction updated the highlighting state. This means the stack at end of line N is up-to-date. + // In case b, the highlighting state at N-1 was up-to-date, and the text of line N was not changed. + // (if the text was changed, the line could not have been reused). + // From this follows that the highlighting state at N is still up-to-date. + + // The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line. + // The HighlightingColorizer will always be asked to highlight the LastDocumentLine of a visual line, so it will always + // invalidate the next visual line when a folded line is constructed and the highlighting stack changed. + + textView.Redraw(line.NextLine, DispatcherPriority.Normal); } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 9e7ff60191..81021b407e 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -262,6 +262,7 @@ TextView.cs + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs index 1c232ca76e..496910faee 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs @@ -398,18 +398,27 @@ namespace ICSharpCode.AvalonEdit.Rendering { VerifyAccess(); bool removedLine = false; + bool changedSomethingBeforeOrInLine = false; for (int i = 0; i < allVisualLines.Count; i++) { VisualLine visualLine = allVisualLines[i]; int lineStart = visualLine.FirstDocumentLine.Offset; int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength; - if (!(lineEnd < offset || lineStart > offset + length)) { - removedLine = true; - allVisualLines.RemoveAt(i--); - DisposeVisualLine(visualLine); + if (offset <= lineEnd) { + changedSomethingBeforeOrInLine = true; + if (offset + length >= lineStart) { + removedLine = true; + allVisualLines.RemoveAt(i--); + DisposeVisualLine(visualLine); + } } } if (removedLine) { visibleVisualLines = null; + } + if (changedSomethingBeforeOrInLine) { + // Repaint not only when something in visible area was changed, but also when anything in front of it + // was changed. We might have to redraw the line number margin. Or the highlighting changed. + // However, we'll try to reuse the existing VisualLines. InvalidateMeasure(redrawPriority); } } @@ -571,13 +580,20 @@ namespace ICSharpCode.AvalonEdit.Rendering /// /// Gets whether the visual lines are valid. - /// Will return false after a call to Redraw(). Accessing the visual lines property - /// will force immediate regeneration of valid lines. + /// Will return false after a call to Redraw(). + /// Accessing the visual lines property will cause a + /// if this property is false. /// public bool VisualLinesValid { get { return visibleVisualLines != null; } } + /// + /// Occurs when the TextView is about to be measured and will regenerate its visual lines. + /// This event may be used to mark visual lines as invalid that would otherwise be reused. + /// + public event EventHandler VisualLineConstructionStarting; + /// /// Occurs when the TextView was measured and changed its visual lines. /// @@ -695,6 +711,9 @@ namespace ICSharpCode.AvalonEdit.Rendering newVisualLines = new List(); + if (VisualLineConstructionStarting != null) + VisualLineConstructionStarting(this, new VisualLineConstructionStartEventArgs(firstLineInView)); + var elementGeneratorsArray = elementGenerators.ToArray(); var lineTransformersArray = lineTransformers.ToArray(); var nextLine = firstLineInView; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineConstructionStartEventArgs.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineConstructionStartEventArgs.cs new file mode 100644 index 0000000000..4bd6fc05d4 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineConstructionStartEventArgs.cs @@ -0,0 +1,33 @@ +// +// +// +// +// $Revision$ +// + +using System; +using ICSharpCode.AvalonEdit.Document; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + /// + /// EventArgs for the event. + /// + public class VisualLineConstructionStartEventArgs : EventArgs + { + /// + /// Gets/Sets the first line that is visible in the TextView. + /// + public DocumentLine FirstLineInView { get; private set; } + + /// + /// Creates a new VisualLineConstructionStartEventArgs instance. + /// + public VisualLineConstructionStartEventArgs(DocumentLine firstLineInView) + { + if (firstLineInView == null) + throw new ArgumentNullException("firstLineInView"); + this.FirstLineInView = firstLineInView; + } + } +}