diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs index 84f0afe0b8..231616a2df 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs @@ -171,6 +171,15 @@ namespace CSharpBinding return null; } + event HighlightingStateChangedEventHandler IHighlighter.HighlightingStateChanged { + add { } + remove { } + } + + void IHighlighter.UpdateHighlightingState(int lineNumber) + { + } + public HighlightedLine HighlightLine(int lineNumber) { IDocumentLine documentLine = textEditor.Document.GetLineByNumber(lineNumber); diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs index 0c9e3952f4..23b7f00af1 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs @@ -145,6 +145,7 @@ namespace ICSharpCode.AvalonEdit.AddIn this.customizations = customizations; this.highlightingDefinition = highlightingDefinition; this.baseHighlighter = baseHighlighter; + baseHighlighter.HighlightingStateChanged += highlighter_HighlightingStateChanged; } public IDocument Document { @@ -155,6 +156,16 @@ namespace ICSharpCode.AvalonEdit.AddIn get { return highlightingDefinition; } } + public event HighlightingStateChangedEventHandler HighlightingStateChanged; + + public void UpdateHighlightingState(int lineNumber) + { + baseHighlighter.UpdateHighlightingState(lineNumber); + foreach (var h in additionalHighlighters) { + h.UpdateHighlightingState(lineNumber); + } + } + public void AddAdditionalHighlighter(IHighlighter highlighter) { if (highlighter == null) @@ -162,11 +173,19 @@ namespace ICSharpCode.AvalonEdit.AddIn if (highlighter.Document != baseHighlighter.Document) throw new ArgumentException("Additional highlighters must use the same document as the base highlighter"); additionalHighlighters.Add(highlighter); + highlighter.HighlightingStateChanged += highlighter_HighlightingStateChanged; } public void RemoveAdditionalHighlighter(IHighlighter highlighter) { additionalHighlighters.Remove(highlighter); + highlighter.HighlightingStateChanged -= highlighter_HighlightingStateChanged; + } + + void highlighter_HighlightingStateChanged(IHighlighter sender, int lineNumber) + { + if (HighlightingStateChanged != null) + HighlightingStateChanged(this, lineNumber); } public IEnumerable GetSpanColorNamesFromLineStart(int lineNumber) @@ -195,7 +214,7 @@ namespace ICSharpCode.AvalonEdit.AddIn { HighlightedLine line = baseHighlighter.HighlightLine(lineNumber); foreach (IHighlighter h in additionalHighlighters) { - MergeHighlighting(line, h.HighlightLine(lineNumber)); + line.MergeWith(h.HighlightLine(lineNumber)); } foreach (HighlightedSection section in line.Sections) { section.Color = CustomizeColor(section.Color); @@ -203,111 +222,6 @@ namespace ICSharpCode.AvalonEdit.AddIn return line; } - #region MergeHighlighting - /// - /// Merges the highlighting sections from additionalLine into line. - /// - void MergeHighlighting(HighlightedLine line, HighlightedLine additionalLine) - { - if (additionalLine == null) - return; - ValidateInvariants(line); - ValidateInvariants(additionalLine); - - int pos = 0; - Stack activeSectionEndOffsets = new Stack(); - int lineEndOffset = line.DocumentLine.EndOffset; - activeSectionEndOffsets.Push(lineEndOffset); - foreach (HighlightedSection newSection in additionalLine.Sections) { - int newSectionStart = newSection.Offset; - // Track the existing sections using the stack, up to the point where - // we need to insert the first part of the newSection - while (pos < line.Sections.Count) { - HighlightedSection s = line.Sections[pos]; - if (newSection.Offset < s.Offset) - break; - while (s.Offset > activeSectionEndOffsets.Peek()) { - activeSectionEndOffsets.Pop(); - } - activeSectionEndOffsets.Push(s.Offset + s.Length); - pos++; - } - // Now insert the new section - // Create a copy of the stack so that we can track the sections we traverse - // during the insertion process: - Stack insertionStack = new Stack(activeSectionEndOffsets.Reverse()); - // The stack enumerator reverses the order of the elements, so we call Reverse() to restore - // the original order. - int i; - for (i = pos; i < line.Sections.Count; i++) { - HighlightedSection s = line.Sections[i]; - if (newSection.Offset + newSection.Length <= s.Offset) - break; - // Insert a segment in front of s: - Insert(line.Sections, ref i, ref newSectionStart, s.Offset, newSection.Color, insertionStack); - - while (s.Offset > insertionStack.Peek()) { - insertionStack.Pop(); - } - insertionStack.Push(s.Offset + s.Length); - } - Insert(line.Sections, ref i, ref newSectionStart, newSection.Offset + newSection.Length, newSection.Color, insertionStack); - } - - ValidateInvariants(line); - } - - void Insert(IList sections, ref int pos, ref int newSectionStart, int insertionEndPos, HighlightingColor color, Stack insertionStack) - { - if (newSectionStart >= insertionEndPos) { - // nothing to insert here - return; - } - - while (insertionStack.Peek() <= newSectionStart) { - insertionStack.Pop(); - } - while (insertionStack.Peek() < insertionEndPos) { - int end = insertionStack.Pop(); - // insert the portion from newSectionStart to end - sections.Insert(pos++, new HighlightedSection { - Offset = newSectionStart, - Length = end - newSectionStart, - Color = color - }); - newSectionStart = end; - } - sections.Insert(pos++, new HighlightedSection { - Offset = newSectionStart, - Length = insertionEndPos - newSectionStart, - Color = color - }); - newSectionStart = insertionEndPos; - } - - [Conditional("DEBUG")] - void ValidateInvariants(HighlightedLine line) - { - int lineStartOffset = line.DocumentLine.Offset; - int lineEndOffset = line.DocumentLine.EndOffset; - for (int i = 0; i < line.Sections.Count; i++) { - HighlightedSection s1 = line.Sections[i]; - if (s1.Offset < lineStartOffset || s1.Length < 0 || s1.Offset + s1.Length > lineEndOffset) - throw new InvalidOperationException("Section is outside line bounds"); - for (int j = i + 1; j < line.Sections.Count; j++) { - HighlightedSection s2 = line.Sections[j]; - if (s2.Offset >= s1.Offset + s1.Length) { - // s2 is after s1 - } else if (s2.Offset >= s1.Offset && s2.Offset + s2.Length <= s1.Offset + s1.Length) { - // s2 is nested within s1 - } else { - throw new InvalidOperationException("Sections are overlapping or incorrectly sorted."); - } - } - } - } - #endregion - HighlightingColor CustomizeColor(HighlightingColor color) { if (color == null || color.Name == null) diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs index c9956d72ac..ef6873d920 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs @@ -147,7 +147,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting CheckIsHighlighting(); isHighlighting = true; try { - HighlightUpTo(lineNumber); + HighlightUpTo(lineNumber - 1); IDocumentLine line = document.GetLineByNumber(lineNumber); highlightedLine = new HighlightedLine(document, line); HighlightLineAndUpdateTreeList(line, lineNumber); @@ -170,13 +170,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting { ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); if (firstInvalidLine <= lineNumber) { - CheckIsHighlighting(); - isHighlighting = true; - try { - HighlightUpTo(lineNumber + 1); - } finally { - isHighlighting = false; - } + UpdateHighlightingState(lineNumber); } return storedSpanStacks[lineNumber]; } @@ -194,10 +188,22 @@ namespace ICSharpCode.AvalonEdit.Highlighting } } + /// + public void UpdateHighlightingState(int lineNumber) + { + CheckIsHighlighting(); + isHighlighting = true; + try { + HighlightUpTo(lineNumber); + } finally { + isHighlighting = false; + } + } + void HighlightUpTo(int targetLineNumber) { - Debug.Assert(highlightedLine == null); // ensure this method is only used for - while (firstInvalidLine < targetLineNumber) { + Debug.Assert(highlightedLine == null); // ensure this method is only outside the actual highlighting logic + while (firstInvalidLine <= targetLineNumber) { HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine); } } @@ -217,7 +223,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting } else { firstInvalidLine = int.MaxValue; } - OnHighlightStateChanged(line, lineNumber); + OnHighlightStateChanged(lineNumber); } else if (firstInvalidLine == lineNumber) { isValid[lineNumber] = true; firstInvalidLine = isValid.IndexOf(false); @@ -228,7 +234,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting static bool EqualSpanStacks(SpanStack a, SpanStack b) { - // We must use value equality between the stacks because TextViewDocumentHighlighter.OnHighlightStateChanged + // We must use value equality between the stacks because HighlightingColorizer.OnHighlightStateChanged // depends on the fact that equal input state + unchanged line contents produce equal output state. if (a == b) return true; @@ -245,14 +251,19 @@ namespace ICSharpCode.AvalonEdit.Highlighting return a.IsEmpty && b.IsEmpty; } + /// + public event HighlightingStateChangedEventHandler HighlightingStateChanged; + /// /// Is called when the highlighting state at the end of the specified line has changed. /// /// This callback must not call HighlightLine or InvalidateHighlighting. /// It may call GetSpanStack, but only for the changed line and lines above. /// This method must not modify the document. - protected virtual void OnHighlightStateChanged(IDocumentLine line, int lineNumber) + protected virtual void OnHighlightStateChanged(int lineNumber) { + if (HighlightingStateChanged != null) + HighlightingStateChanged(this, lineNumber); } #region Highlighting Engine diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs index 98cd9e882d..c5530dd6d7 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs @@ -3,9 +3,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; - +using System.Linq; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.NRefactory.Editor; @@ -49,7 +50,113 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// public IList Sections { get; private set; } + [Conditional("DEBUG")] + void ValidateInvariants() + { + var line = this; + int lineStartOffset = line.DocumentLine.Offset; + int lineEndOffset = line.DocumentLine.EndOffset; + for (int i = 0; i < line.Sections.Count; i++) { + HighlightedSection s1 = line.Sections[i]; + if (s1.Offset < lineStartOffset || s1.Length < 0 || s1.Offset + s1.Length > lineEndOffset) + throw new InvalidOperationException("Section is outside line bounds"); + for (int j = i + 1; j < line.Sections.Count; j++) { + HighlightedSection s2 = line.Sections[j]; + if (s2.Offset >= s1.Offset + s1.Length) { + // s2 is after s1 + } else if (s2.Offset >= s1.Offset && s2.Offset + s2.Length <= s1.Offset + s1.Length) { + // s2 is nested within s1 + } else { + throw new InvalidOperationException("Sections are overlapping or incorrectly sorted."); + } + } + } + } + + #region Merge + /// + /// Merges the additional line into this line. + /// + public void MergeWith(HighlightedLine additionalLine) + { + if (additionalLine == null) + return; + ValidateInvariants(); + additionalLine.ValidateInvariants(); + + int pos = 0; + Stack activeSectionEndOffsets = new Stack(); + int lineEndOffset = this.DocumentLine.EndOffset; + activeSectionEndOffsets.Push(lineEndOffset); + foreach (HighlightedSection newSection in additionalLine.Sections) { + int newSectionStart = newSection.Offset; + // Track the existing sections using the stack, up to the point where + // we need to insert the first part of the newSection + while (pos < this.Sections.Count) { + HighlightedSection s = this.Sections[pos]; + if (newSection.Offset < s.Offset) + break; + while (s.Offset > activeSectionEndOffsets.Peek()) { + activeSectionEndOffsets.Pop(); + } + activeSectionEndOffsets.Push(s.Offset + s.Length); + pos++; + } + // Now insert the new section + // Create a copy of the stack so that we can track the sections we traverse + // during the insertion process: + Stack insertionStack = new Stack(activeSectionEndOffsets.Reverse()); + // The stack enumerator reverses the order of the elements, so we call Reverse() to restore + // the original order. + int i; + for (i = pos; i < this.Sections.Count; i++) { + HighlightedSection s = this.Sections[i]; + if (newSection.Offset + newSection.Length <= s.Offset) + break; + // Insert a segment in front of s: + Insert(ref i, ref newSectionStart, s.Offset, newSection.Color, insertionStack); + + while (s.Offset > insertionStack.Peek()) { + insertionStack.Pop(); + } + insertionStack.Push(s.Offset + s.Length); + } + Insert(ref i, ref newSectionStart, newSection.Offset + newSection.Length, newSection.Color, insertionStack); + } + + ValidateInvariants(); + } + + void Insert(ref int pos, ref int newSectionStart, int insertionEndPos, HighlightingColor color, Stack insertionStack) + { + if (newSectionStart >= insertionEndPos) { + // nothing to insert here + return; + } + + while (insertionStack.Peek() <= newSectionStart) { + insertionStack.Pop(); + } + while (insertionStack.Peek() < insertionEndPos) { + int end = insertionStack.Pop(); + // insert the portion from newSectionStart to end + this.Sections.Insert(pos++, new HighlightedSection { + Offset = newSectionStart, + Length = end - newSectionStart, + Color = color + }); + newSectionStart = end; + } + this.Sections.Insert(pos++, new HighlightedSection { + Offset = newSectionStart, + Length = insertionEndPos - newSectionStart, + Color = color + }); + newSectionStart = insertionEndPos; + } + #endregion + #region ToHtml sealed class HtmlElement : IComparable { internal readonly int Offset; @@ -146,5 +253,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting { return "[" + GetType().Name + " " + ToHtml(new HtmlOptions()) + "]"; } + #endregion } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs index 2f4160c2ad..5abeaad61f 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs @@ -18,6 +18,8 @@ namespace ICSharpCode.AvalonEdit.Highlighting public class HighlightingColorizer : DocumentColorizingTransformer { readonly HighlightingRuleSet ruleSet; + TextView textView; + IHighlighter highlighter; /// /// Creates a new HighlightingColorizer instance. @@ -43,8 +45,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// protected virtual void DeregisterServices(TextView textView) { - // remove existing highlighter, if any exists - textView.Services.RemoveService(typeof(IHighlighter)); + if (highlighter != null) { + highlighter.HighlightingStateChanged -= OnHighlightStateChanged; + // remove highlighter if it is registered + if (textView.Services.GetService(typeof(IHighlighter)) == highlighter) + textView.Services.RemoveService(typeof(IHighlighter)); + + } } /// @@ -53,10 +60,15 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// protected virtual void RegisterServices(TextView textView) { - TextDocument document = textView.Document; - if (document != null) { - IHighlighter highlighter = CreateHighlighter(textView, document); - textView.Services.AddService(typeof(IHighlighter), highlighter); + if (textView.Document != null) { + highlighter = textView.Document != null ? CreateHighlighter(textView, textView.Document) : null; + if (highlighter != null) { + // add service only if it doesn't already exist + if (textView.Services.GetService(typeof(IHighlighter)) == null) { + textView.Services.AddService(typeof(IHighlighter), highlighter); + } + highlighter.HighlightingStateChanged += OnHighlightStateChanged; + } } } @@ -65,13 +77,17 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// protected virtual IHighlighter CreateHighlighter(TextView textView, TextDocument document) { - return new TextViewDocumentHighlighter(this, textView, document, ruleSet); + return new DocumentHighlighter(document, ruleSet); } /// protected override void OnAddToTextView(TextView textView) { + if (this.textView != null) { + throw new InvalidOperationException("Cannot use a HighlightingColorizer instance in multiple text views. Please create a separate instance for each text view."); + } base.OnAddToTextView(textView); + this.textView = textView; textView.DocumentChanged += textView_DocumentChanged; textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting; RegisterServices(textView); @@ -84,18 +100,18 @@ namespace ICSharpCode.AvalonEdit.Highlighting textView.DocumentChanged -= textView_DocumentChanged; textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting; base.OnRemoveFromTextView(textView); + this.textView = null; } 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. lineNumberBeingColorized = e.FirstLineInView.LineNumber - 1; - highlighter.GetColorStack(lineNumberBeingColorized); + highlighter.UpdateHighlightingState(lineNumberBeingColorized); lineNumberBeingColorized = 0; } } @@ -108,14 +124,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting this.lastColorizedLine = null; base.Colorize(context); if (this.lastColorizedLine != context.VisualLine.LastDocumentLine) { - IHighlighter highlighter = context.TextView.Services.GetService(typeof(IHighlighter)) as IHighlighter; if (highlighter != null) { // In some cases, it is possible that we didn't highlight the last document line within the visual line // (e.g. when the line ends with a fold marker). // But even if we didn't highlight it, we'll have to update the highlighting state for it so that the // proof inside TextViewDocumentHighlighter.OnHighlightStateChanged holds. lineNumberBeingColorized = context.VisualLine.LastDocumentLine.LineNumber; - highlighter.GetColorStack(lineNumberBeingColorized); + highlighter.UpdateHighlightingState(lineNumberBeingColorized); lineNumberBeingColorized = 0; } } @@ -127,7 +142,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// protected override void ColorizeLine(DocumentLine line) { - IHighlighter highlighter = CurrentContext.TextView.Services.GetService(typeof(IHighlighter)) as IHighlighter; if (highlighter != null) { lineNumberBeingColorized = line.LineNumber; HighlightedLine hl = highlighter.HighlightLine(lineNumberBeingColorized); @@ -181,102 +195,82 @@ namespace ICSharpCode.AvalonEdit.Highlighting } /// - /// This class is responsible for telling the TextView to redraw lines when the highlighting state has changed. + /// This method is responsible for telling the TextView to redraw lines when the highlighting state has changed. /// /// /// Creation of a VisualLine triggers the syntax highlighter (which works on-demand), so it says: /// Hey, the user typed "/*". Don't just recreate that line, but also the next one /// because my highlighting state (at end of line) changed! /// - sealed class TextViewDocumentHighlighter : DocumentHighlighter + void OnHighlightStateChanged(IHighlighter sender, int lineNumber) { - readonly HighlightingColorizer colorizer; - readonly TextView textView; - - public TextViewDocumentHighlighter(HighlightingColorizer colorizer, TextView textView, TextDocument document, HighlightingRuleSet baseRuleSet) - : base(document, baseRuleSet) - { - Debug.Assert(colorizer != null); - Debug.Assert(textView != null); - this.colorizer = colorizer; - this.textView = textView; + if (lineNumberBeingColorized != lineNumber) { + // Ignore notifications for any line except the one we're interested in. + // This improves the performance as Redraw() can take quite some time when called repeatedly + // while scanning the document (above the visible area) for highlighting changes. + return; } - protected override void OnHighlightStateChanged(IDocumentLine line, int lineNumber) - { - base.OnHighlightStateChanged(line, lineNumber); - if (colorizer.lineNumberBeingColorized != lineNumber) { - // Ignore notifications for any line except the one we're interested in. - // This improves the performance as Redraw() can take quite some time when called repeatedly - // while scanning the document (above the visible area) for highlighting changes. - return; - } - 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 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. - // Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine, - // so it will always invalidate the next visual line when a folded line is constructed - // and the highlighting stack has changed. - - textView.Redraw(line.NextLine, DispatcherPriority.Normal); - - /* - * Meta-comment: "why does this have to be so complicated?" - * - * The problem is that I want to re-highlight only on-demand and incrementally; - * and at the same time only repaint changed lines. - * So the highlighter and the VisualLine construction both have to run in a single pass. - * The highlighter must take care that it never touches already constructed visual lines; - * if it detects that something must be redrawn because the highlighting state changed, - * it must do so early enough in the construction process. - * But doing it too early means it doesn't have the information necessary to re-highlight and redraw only the desired parts. - */ - } + // 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 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. + // Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine, + // so it will always invalidate the next visual line when a folded line is constructed + // and the highlighting stack has changed. + + if (lineNumber + 1 <= textView.Document.LineCount) + textView.Redraw(textView.Document.GetLineByNumber(lineNumber + 1), DispatcherPriority.Normal); + + /* + * Meta-comment: "why does this have to be so complicated?" + * + * The problem is that I want to re-highlight only on-demand and incrementally; + * and at the same time only repaint changed lines. + * So the highlighter and the VisualLine construction both have to run in a single pass. + * The highlighter must take care that it never touches already constructed visual lines; + * if it detects that something must be redrawn because the highlighting state changed, + * it must do so early enough in the construction process. + * But doing it too early means it doesn't have the information necessary to re-highlight and redraw only the desired parts. + */ } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs index 55a6f70e97..10561b91d3 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs @@ -37,5 +37,34 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// The line to highlight. /// A line object that represents the highlighted sections. HighlightedLine HighlightLine(int lineNumber); + + /// + /// Enforces a highlighting state update (triggering the HighlightingStateChanged event if necessary) + /// for all lines up to (and inclusive) the specified line number. + /// + void UpdateHighlightingState(int lineNumber); + + /// + /// Notification when the highlighter detects that the highlighting state at the end of a line + /// has changed. + /// This event gets raised for each line as it is processed by the highlighter + /// unless the highlighting state for the line is equal to the old state (when the same line was highlighted previously). + /// + /// + /// For implementers: there is the requirement that, if there was no state changed reported at line X, + /// and there were no document changes between line X and Y (with Y > X), then + /// this event must not be raised for any line between X and Y. + /// + /// Equal input state + unchanged line = Equal output state. + /// + /// See the comment in the HighlightingColorizer.OnHighlightStateChanged implementation + /// for details about the requirements for a correct custom IHighlighter. + /// + event HighlightingStateChangedEventHandler HighlightingStateChanged; } + + /// + /// Event handler for + /// + public delegate void HighlightingStateChangedEventHandler(IHighlighter sender, int lineNumber); }