Browse Source

Simplify HighlightingColorizer by supporting only a single TextView per colorizer instance; and by exposing the HighlightingStateChanged callback as an event.

Move the HighlightedLine merging logic into AvalonEdit.
newNRvisualizers
Daniel Grunwald 14 years ago
parent
commit
39d91dad79
  1. 9
      src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs
  2. 126
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs
  3. 37
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs
  4. 110
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs
  5. 192
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs
  6. 29
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs

9
src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs

@ -171,6 +171,15 @@ namespace CSharpBinding
return null; return null;
} }
event HighlightingStateChangedEventHandler IHighlighter.HighlightingStateChanged {
add { }
remove { }
}
void IHighlighter.UpdateHighlightingState(int lineNumber)
{
}
public HighlightedLine HighlightLine(int lineNumber) public HighlightedLine HighlightLine(int lineNumber)
{ {
IDocumentLine documentLine = textEditor.Document.GetLineByNumber(lineNumber); IDocumentLine documentLine = textEditor.Document.GetLineByNumber(lineNumber);

126
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs

@ -145,6 +145,7 @@ namespace ICSharpCode.AvalonEdit.AddIn
this.customizations = customizations; this.customizations = customizations;
this.highlightingDefinition = highlightingDefinition; this.highlightingDefinition = highlightingDefinition;
this.baseHighlighter = baseHighlighter; this.baseHighlighter = baseHighlighter;
baseHighlighter.HighlightingStateChanged += highlighter_HighlightingStateChanged;
} }
public IDocument Document { public IDocument Document {
@ -155,6 +156,16 @@ namespace ICSharpCode.AvalonEdit.AddIn
get { return highlightingDefinition; } 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) public void AddAdditionalHighlighter(IHighlighter highlighter)
{ {
if (highlighter == null) if (highlighter == null)
@ -162,11 +173,19 @@ namespace ICSharpCode.AvalonEdit.AddIn
if (highlighter.Document != baseHighlighter.Document) if (highlighter.Document != baseHighlighter.Document)
throw new ArgumentException("Additional highlighters must use the same document as the base highlighter"); throw new ArgumentException("Additional highlighters must use the same document as the base highlighter");
additionalHighlighters.Add(highlighter); additionalHighlighters.Add(highlighter);
highlighter.HighlightingStateChanged += highlighter_HighlightingStateChanged;
} }
public void RemoveAdditionalHighlighter(IHighlighter highlighter) public void RemoveAdditionalHighlighter(IHighlighter highlighter)
{ {
additionalHighlighters.Remove(highlighter); additionalHighlighters.Remove(highlighter);
highlighter.HighlightingStateChanged -= highlighter_HighlightingStateChanged;
}
void highlighter_HighlightingStateChanged(IHighlighter sender, int lineNumber)
{
if (HighlightingStateChanged != null)
HighlightingStateChanged(this, lineNumber);
} }
public IEnumerable<string> GetSpanColorNamesFromLineStart(int lineNumber) public IEnumerable<string> GetSpanColorNamesFromLineStart(int lineNumber)
@ -195,7 +214,7 @@ namespace ICSharpCode.AvalonEdit.AddIn
{ {
HighlightedLine line = baseHighlighter.HighlightLine(lineNumber); HighlightedLine line = baseHighlighter.HighlightLine(lineNumber);
foreach (IHighlighter h in additionalHighlighters) { foreach (IHighlighter h in additionalHighlighters) {
MergeHighlighting(line, h.HighlightLine(lineNumber)); line.MergeWith(h.HighlightLine(lineNumber));
} }
foreach (HighlightedSection section in line.Sections) { foreach (HighlightedSection section in line.Sections) {
section.Color = CustomizeColor(section.Color); section.Color = CustomizeColor(section.Color);
@ -203,111 +222,6 @@ namespace ICSharpCode.AvalonEdit.AddIn
return line; return line;
} }
#region MergeHighlighting
/// <summary>
/// Merges the highlighting sections from additionalLine into line.
/// </summary>
void MergeHighlighting(HighlightedLine line, HighlightedLine additionalLine)
{
if (additionalLine == null)
return;
ValidateInvariants(line);
ValidateInvariants(additionalLine);
int pos = 0;
Stack<int> activeSectionEndOffsets = new Stack<int>();
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<int> insertionStack = new Stack<int>(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<HighlightedSection> sections, ref int pos, ref int newSectionStart, int insertionEndPos, HighlightingColor color, Stack<int> 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) HighlightingColor CustomizeColor(HighlightingColor color)
{ {
if (color == null || color.Name == null) if (color == null || color.Name == null)

37
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs

@ -147,7 +147,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
CheckIsHighlighting(); CheckIsHighlighting();
isHighlighting = true; isHighlighting = true;
try { try {
HighlightUpTo(lineNumber); HighlightUpTo(lineNumber - 1);
IDocumentLine line = document.GetLineByNumber(lineNumber); IDocumentLine line = document.GetLineByNumber(lineNumber);
highlightedLine = new HighlightedLine(document, line); highlightedLine = new HighlightedLine(document, line);
HighlightLineAndUpdateTreeList(line, lineNumber); HighlightLineAndUpdateTreeList(line, lineNumber);
@ -170,13 +170,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
{ {
ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount);
if (firstInvalidLine <= lineNumber) { if (firstInvalidLine <= lineNumber) {
CheckIsHighlighting(); UpdateHighlightingState(lineNumber);
isHighlighting = true;
try {
HighlightUpTo(lineNumber + 1);
} finally {
isHighlighting = false;
}
} }
return storedSpanStacks[lineNumber]; return storedSpanStacks[lineNumber];
} }
@ -194,10 +188,22 @@ namespace ICSharpCode.AvalonEdit.Highlighting
} }
} }
/// <inheritdoc/>
public void UpdateHighlightingState(int lineNumber)
{
CheckIsHighlighting();
isHighlighting = true;
try {
HighlightUpTo(lineNumber);
} finally {
isHighlighting = false;
}
}
void HighlightUpTo(int targetLineNumber) void HighlightUpTo(int targetLineNumber)
{ {
Debug.Assert(highlightedLine == null); // ensure this method is only used for Debug.Assert(highlightedLine == null); // ensure this method is only outside the actual highlighting logic
while (firstInvalidLine < targetLineNumber) { while (firstInvalidLine <= targetLineNumber) {
HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine); HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine);
} }
} }
@ -217,7 +223,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
} else { } else {
firstInvalidLine = int.MaxValue; firstInvalidLine = int.MaxValue;
} }
OnHighlightStateChanged(line, lineNumber); OnHighlightStateChanged(lineNumber);
} else if (firstInvalidLine == lineNumber) { } else if (firstInvalidLine == lineNumber) {
isValid[lineNumber] = true; isValid[lineNumber] = true;
firstInvalidLine = isValid.IndexOf(false); firstInvalidLine = isValid.IndexOf(false);
@ -228,7 +234,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
static bool EqualSpanStacks(SpanStack a, SpanStack b) 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. // depends on the fact that equal input state + unchanged line contents produce equal output state.
if (a == b) if (a == b)
return true; return true;
@ -245,14 +251,19 @@ namespace ICSharpCode.AvalonEdit.Highlighting
return a.IsEmpty && b.IsEmpty; return a.IsEmpty && b.IsEmpty;
} }
/// <inheritdoc/>
public event HighlightingStateChangedEventHandler HighlightingStateChanged;
/// <summary> /// <summary>
/// Is called when the highlighting state at the end of the specified line has changed. /// Is called when the highlighting state at the end of the specified line has changed.
/// </summary> /// </summary>
/// <remarks>This callback must not call HighlightLine or InvalidateHighlighting. /// <remarks>This callback must not call HighlightLine or InvalidateHighlighting.
/// It may call GetSpanStack, but only for the changed line and lines above. /// It may call GetSpanStack, but only for the changed line and lines above.
/// This method must not modify the document.</remarks> /// This method must not modify the document.</remarks>
protected virtual void OnHighlightStateChanged(IDocumentLine line, int lineNumber) protected virtual void OnHighlightStateChanged(int lineNumber)
{ {
if (HighlightingStateChanged != null)
HighlightingStateChanged(this, lineNumber);
} }
#region Highlighting Engine #region Highlighting Engine

110
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs

@ -3,9 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.AvalonEdit.Utils;
using ICSharpCode.NRefactory.Editor; using ICSharpCode.NRefactory.Editor;
@ -49,7 +50,113 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary> /// </summary>
public IList<HighlightedSection> Sections { get; private set; } public IList<HighlightedSection> 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
/// <summary>
/// Merges the additional line into this line.
/// </summary>
public void MergeWith(HighlightedLine additionalLine)
{
if (additionalLine == null)
return;
ValidateInvariants();
additionalLine.ValidateInvariants();
int pos = 0;
Stack<int> activeSectionEndOffsets = new Stack<int>();
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<int> insertionStack = new Stack<int>(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<int> 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<HtmlElement> sealed class HtmlElement : IComparable<HtmlElement>
{ {
internal readonly int Offset; internal readonly int Offset;
@ -146,5 +253,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting
{ {
return "[" + GetType().Name + " " + ToHtml(new HtmlOptions()) + "]"; return "[" + GetType().Name + " " + ToHtml(new HtmlOptions()) + "]";
} }
#endregion
} }
} }

192
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs

@ -18,6 +18,8 @@ namespace ICSharpCode.AvalonEdit.Highlighting
public class HighlightingColorizer : DocumentColorizingTransformer public class HighlightingColorizer : DocumentColorizingTransformer
{ {
readonly HighlightingRuleSet ruleSet; readonly HighlightingRuleSet ruleSet;
TextView textView;
IHighlighter highlighter;
/// <summary> /// <summary>
/// Creates a new HighlightingColorizer instance. /// Creates a new HighlightingColorizer instance.
@ -43,8 +45,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary> /// </summary>
protected virtual void DeregisterServices(TextView textView) protected virtual void DeregisterServices(TextView textView)
{ {
// remove existing highlighter, if any exists if (highlighter != null) {
textView.Services.RemoveService(typeof(IHighlighter)); highlighter.HighlightingStateChanged -= OnHighlightStateChanged;
// remove highlighter if it is registered
if (textView.Services.GetService(typeof(IHighlighter)) == highlighter)
textView.Services.RemoveService(typeof(IHighlighter));
}
} }
/// <summary> /// <summary>
@ -53,10 +60,15 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary> /// </summary>
protected virtual void RegisterServices(TextView textView) protected virtual void RegisterServices(TextView textView)
{ {
TextDocument document = textView.Document; if (textView.Document != null) {
if (document != null) { highlighter = textView.Document != null ? CreateHighlighter(textView, textView.Document) : null;
IHighlighter highlighter = CreateHighlighter(textView, document); if (highlighter != null) {
textView.Services.AddService(typeof(IHighlighter), highlighter); // 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
/// </summary> /// </summary>
protected virtual IHighlighter CreateHighlighter(TextView textView, TextDocument document) protected virtual IHighlighter CreateHighlighter(TextView textView, TextDocument document)
{ {
return new TextViewDocumentHighlighter(this, textView, document, ruleSet); return new DocumentHighlighter(document, ruleSet);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAddToTextView(TextView textView) 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); base.OnAddToTextView(textView);
this.textView = textView;
textView.DocumentChanged += textView_DocumentChanged; textView.DocumentChanged += textView_DocumentChanged;
textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting; textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting;
RegisterServices(textView); RegisterServices(textView);
@ -84,18 +100,18 @@ namespace ICSharpCode.AvalonEdit.Highlighting
textView.DocumentChanged -= textView_DocumentChanged; textView.DocumentChanged -= textView_DocumentChanged;
textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting; textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting;
base.OnRemoveFromTextView(textView); base.OnRemoveFromTextView(textView);
this.textView = null;
} }
void textView_VisualLineConstructionStarting(object sender, VisualLineConstructionStartEventArgs e) void textView_VisualLineConstructionStarting(object sender, VisualLineConstructionStartEventArgs e)
{ {
IHighlighter highlighter = ((TextView)sender).Services.GetService(typeof(IHighlighter)) as IHighlighter;
if (highlighter != null) { if (highlighter != null) {
// Force update of highlighting state up to the position where we start generating visual lines. // 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. // 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) // 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. // before the visual line construction reuses existing lines that were built using the invalid highlighting state.
lineNumberBeingColorized = e.FirstLineInView.LineNumber - 1; lineNumberBeingColorized = e.FirstLineInView.LineNumber - 1;
highlighter.GetColorStack(lineNumberBeingColorized); highlighter.UpdateHighlightingState(lineNumberBeingColorized);
lineNumberBeingColorized = 0; lineNumberBeingColorized = 0;
} }
} }
@ -108,14 +124,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting
this.lastColorizedLine = null; this.lastColorizedLine = null;
base.Colorize(context); base.Colorize(context);
if (this.lastColorizedLine != context.VisualLine.LastDocumentLine) { if (this.lastColorizedLine != context.VisualLine.LastDocumentLine) {
IHighlighter highlighter = context.TextView.Services.GetService(typeof(IHighlighter)) as IHighlighter;
if (highlighter != null) { if (highlighter != null) {
// In some cases, it is possible that we didn't highlight the last document line within the visual line // 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). // (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 // 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. // proof inside TextViewDocumentHighlighter.OnHighlightStateChanged holds.
lineNumberBeingColorized = context.VisualLine.LastDocumentLine.LineNumber; lineNumberBeingColorized = context.VisualLine.LastDocumentLine.LineNumber;
highlighter.GetColorStack(lineNumberBeingColorized); highlighter.UpdateHighlightingState(lineNumberBeingColorized);
lineNumberBeingColorized = 0; lineNumberBeingColorized = 0;
} }
} }
@ -127,7 +142,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// <inheritdoc/> /// <inheritdoc/>
protected override void ColorizeLine(DocumentLine line) protected override void ColorizeLine(DocumentLine line)
{ {
IHighlighter highlighter = CurrentContext.TextView.Services.GetService(typeof(IHighlighter)) as IHighlighter;
if (highlighter != null) { if (highlighter != null) {
lineNumberBeingColorized = line.LineNumber; lineNumberBeingColorized = line.LineNumber;
HighlightedLine hl = highlighter.HighlightLine(lineNumberBeingColorized); HighlightedLine hl = highlighter.HighlightLine(lineNumberBeingColorized);
@ -181,102 +195,82 @@ namespace ICSharpCode.AvalonEdit.Highlighting
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Creation of a VisualLine triggers the syntax highlighter (which works on-demand), so it says: /// 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 /// Hey, the user typed "/*". Don't just recreate that line, but also the next one
/// because my highlighting state (at end of line) changed! /// because my highlighting state (at end of line) changed!
/// </remarks> /// </remarks>
sealed class TextViewDocumentHighlighter : DocumentHighlighter void OnHighlightStateChanged(IHighlighter sender, int lineNumber)
{ {
readonly HighlightingColorizer colorizer; if (lineNumberBeingColorized != lineNumber) {
readonly TextView textView; // 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
public TextViewDocumentHighlighter(HighlightingColorizer colorizer, TextView textView, TextDocument document, HighlightingRuleSet baseRuleSet) // while scanning the document (above the visible area) for highlighting changes.
: base(document, baseRuleSet) return;
{
Debug.Assert(colorizer != null);
Debug.Assert(textView != null);
this.colorizer = colorizer;
this.textView = textView;
} }
protected override void OnHighlightStateChanged(IDocumentLine line, int lineNumber) // 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.
base.OnHighlightStateChanged(line, lineNumber); // If the highlighting state change applies to the lines below, too, the construction of each line
if (colorizer.lineNumberBeingColorized != lineNumber) { // will invalidate the next line, and the construction pass will regenerate all lines.
// 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 Debug.WriteLine("OnHighlightStateChanged forces redraw of line " + (lineNumber + 1));
// while scanning the document (above the visible area) for highlighting changes.
return; // If the VisualLine construction is in progress, we have to avoid sending redraw commands for
} // anything above the line currently being constructed.
if (textView.Document != this.Document) { // It takes some explanation to see why this cannot happen.
// May happen if document on text view was changed but some user code is still using the // VisualLines always get constructed from top to bottom.
// existing IHighlighter instance. // Each VisualLine construction calls into the highlighter and thus forces an update of the
return; // 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
// The user may have inserted "/*" into the current line, and so far only that line got redrawn. // a VisualLine is being reused, the highlighting state at that location is still up-to-date.
// 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 // This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes
// will invalidate the next line, and the construction pass will regenerate all lines. // (e.g. split view).
Debug.WriteLine("OnHighlightStateChanged forces redraw of line " + (lineNumber + 1)); // 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
// If the VisualLine construction is in progress, we have to avoid sending redraw commands for // in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused.
// anything above the line currently being constructed.
// It takes some explanation to see why this cannot happen. // Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom
// VisualLines always get constructed from top to bottom. // construction process.
// 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. // We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1.
// To guarantee that we don't redraw lines we just constructed, we need to show that when // Start of induction: the first line in view is reused only if the highlighting state was up-to-date
// a VisualLine is being reused, the highlighting state at that location is still up-to-date. // until line N-1 (no change detected in VisualLineConstructionStarting event).
// This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes // Induction step:
// (e.g. split view). // If another line N+1 is being reused, then either
// a) the previous line (the visual line containing document line N) was newly constructed
// For the first line in the view, the TextView.VisualLineConstructionStarting event is used to check that the // or b) the previous line was reused
// highlighting state is up-to-date. If it isn't, this method will be executed, and it'll mark the first line // In case a, the construction updated the highlighting state. This means the stack at end of line N is up-to-date.
// in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused. // 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).
// Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom // From this follows that the highlighting state at N is still up-to-date.
// construction process.
// The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line.
// We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1. // 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
// Start of induction: the first line in view is reused only if the highlighting state was up-to-date // and the highlighting stack has changed.
// until line N-1 (no change detected in VisualLineConstructionStarting event).
if (lineNumber + 1 <= textView.Document.LineCount)
// Induction step: textView.Redraw(textView.Document.GetLineByNumber(lineNumber + 1), DispatcherPriority.Normal);
// 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 * Meta-comment: "why does this have to be so complicated?"
// 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. * The problem is that I want to re-highlight only on-demand and incrementally;
// (if the text was changed, the line could not have been reused). * and at the same time only repaint changed lines.
// From this follows that the highlighting state at N is still up-to-date. * 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;
// The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line. * if it detects that something must be redrawn because the highlighting state changed,
// Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine, * it must do so early enough in the construction process.
// so it will always invalidate the next visual line when a folded line is constructed * But doing it too early means it doesn't have the information necessary to re-highlight and redraw only the desired parts.
// 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.
*/
}
} }
} }
} }

29
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs

@ -37,5 +37,34 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// <param name="lineNumber">The line to highlight.</param> /// <param name="lineNumber">The line to highlight.</param>
/// <returns>A <see cref="HighlightedLine"/> line object that represents the highlighted sections.</returns> /// <returns>A <see cref="HighlightedLine"/> line object that represents the highlighted sections.</returns>
HighlightedLine HighlightLine(int lineNumber); HighlightedLine HighlightLine(int lineNumber);
/// <summary>
/// Enforces a highlighting state update (triggering the HighlightingStateChanged event if necessary)
/// for all lines up to (and inclusive) the specified line number.
/// </summary>
void UpdateHighlightingState(int lineNumber);
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
event HighlightingStateChangedEventHandler HighlightingStateChanged;
} }
/// <summary>
/// Event handler for <see cref="IHighlighter.HighlightingStateChanged"/>
/// </summary>
public delegate void HighlightingStateChangedEventHandler(IHighlighter sender, int lineNumber);
} }

Loading…
Cancel
Save