// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using SpanStack = ICSharpCode.AvalonEdit.Utils.ImmutableStack<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// This class can syntax-highlight a document.
/// It automatically manages invalidating the highlighting when the document changes.
/// </summary>
public class DocumentHighlighter : ILineTracker, IHighlighter
{
/// <summary>
/// Stores the span state at the end of each line.
/// storedSpanStacks[0] = state at beginning of document
/// storedSpanStacks[i] = state after line i
/// </summary>
readonly CompressingTreeList<SpanStack> storedSpanStacks = new CompressingTreeList<SpanStack>(object.ReferenceEquals);
readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b);
readonly TextDocument document;
readonly HighlightingRuleSet baseRuleSet;
bool isHighlighting;
/// <summary>
/// Gets the document that this DocumentHighlighter is highlighting.
/// </summary>
public TextDocument Document {
get { return document; }
}
/// <summary>
/// Creates a new DocumentHighlighter instance.
/// </summary>
public DocumentHighlighter(TextDocument document, HighlightingRuleSet baseRuleSet)
{
if (document == null)
throw new ArgumentNullException("document");
if (baseRuleSet == null)
throw new ArgumentNullException("baseRuleSet");
this.document = document;
this.baseRuleSet = baseRuleSet;
WeakLineTracker.Register(document, this);
InvalidateHighlighting();
}
void ILineTracker.BeforeRemoveLine(DocumentLine line)
{
CheckIsHighlighting();
int number = line.LineNumber;
storedSpanStacks.RemoveAt(number);
isValid.RemoveAt(number);
if (number < isValid.Count) {
isValid[number] = false;
if (number < firstInvalidLine)
firstInvalidLine = number;
}
}
void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength)
{
CheckIsHighlighting();
int number = line.LineNumber;
isValid[number] = false;
if (number < firstInvalidLine)
firstInvalidLine = number;
}
void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine)
{
CheckIsHighlighting();
Debug.Assert(insertionPos.LineNumber + 1 == newLine.LineNumber);
int lineNumber = newLine.LineNumber;
storedSpanStacks.Insert(lineNumber, null);
isValid.Insert(lineNumber, false);
if (lineNumber < firstInvalidLine)
firstInvalidLine = lineNumber;
}
void ILineTracker.RebuildDocument()
{
InvalidateHighlighting();
}
ImmutableStack<HighlightingSpan> initialSpanStack = SpanStack.Empty;
/// <summary>
/// Gets/sets the the initial span stack of the document. Default value is <see cref="SpanStack.Empty" />.
/// </summary>
public ImmutableStack<HighlightingSpan> InitialSpanStack {
get { return initialSpanStack; }
set {
if (value == null)
initialSpanStack = SpanStack.Empty;
else
initialSpanStack = value;
InvalidateHighlighting();
}
}
/// <summary>
/// Invalidates all stored highlighting info.
/// When the document changes, the highlighting is invalidated automatically, this method
/// needs to be called only when there are changes to the highlighting rule set.
/// </summary>
public void InvalidateHighlighting()
{
CheckIsHighlighting();
storedSpanStacks.Clear();
storedSpanStacks.Add(initialSpanStack);
storedSpanStacks.InsertRange(1, document.LineCount, null);
isValid.Clear();
isValid.Add(true);
isValid.InsertRange(1, document.LineCount, false);
firstInvalidLine = 1;
}
int firstInvalidLine;
/// <summary>
/// Highlights the specified document line.
/// </summary>
/// <param name="line">The line to highlight.</param>
/// <returns>A <see cref="HighlightedLine"/> line object that represents the highlighted sections.</returns>
[ObsoleteAttribute("Use the (int lineNumber) overload instead")]
public HighlightedLine HighlightLine(DocumentLine line)
{
if (!document.Lines.Contains(line))
throw new ArgumentException("The specified line does not belong to the document.");
return HighlightLine(line.LineNumber);
}
/// <inheritdoc/>
public HighlightedLine HighlightLine(int lineNumber)
{
ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 1, document.LineCount);
CheckIsHighlighting();
isHighlighting = true;
try {
HighlightUpTo(lineNumber);
DocumentLine line = document.GetLineByNumber(lineNumber);
highlightedLine = new HighlightedLine(document, line);
HighlightLineAndUpdateTreeList(line, lineNumber);
return highlightedLine;
} finally {
highlightedLine = null;
isHighlighting = false;
}
}
/// <inheritdoc/>
public SpanStack GetSpanStack(int lineNumber)
{
ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount);
if (firstInvalidLine <= lineNumber) {
CheckIsHighlighting();
isHighlighting = true;
try {
HighlightUpTo(lineNumber + 1);
} finally {
isHighlighting = false;
}
}
return storedSpanStacks[lineNumber];
}
void CheckIsHighlighting()
{
if (isHighlighting) {
throw new InvalidOperationException("Invalid call - a highlighting operation is currently running.");
}
}
void HighlightUpTo(int targetLineNumber)
{
Debug.Assert(highlightedLine == null); // ensure this method is only used for
while (firstInvalidLine < targetLineNumber) {
HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine);
}
}
void HighlightLineAndUpdateTreeList(DocumentLine line, int lineNumber)
{
//Debug.WriteLine("Highlight line " + lineNumber + (highlightedLine != null ? "" : " (span stack only)"));
spanStack = storedSpanStacks[lineNumber - 1];
HighlightLineInternal(line);
if (!EqualSpanStacks(spanStack, storedSpanStacks[lineNumber])) {
isValid[lineNumber] = true;
//Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack);
storedSpanStacks[lineNumber] = spanStack;
if (lineNumber + 1 < isValid.Count) {
isValid[lineNumber + 1] = false;
firstInvalidLine = lineNumber + 1;
} else {
firstInvalidLine = int.MaxValue;
}
OnHighlightStateChanged(line, lineNumber);
} else if (firstInvalidLine == lineNumber) {
isValid[lineNumber] = true;
firstInvalidLine = isValid.IndexOf(false);
if (firstInvalidLine < 0)
firstInvalidLine = int.MaxValue;
}
}
static bool EqualSpanStacks(SpanStack a, SpanStack b)
{
// We must use value equality between the stacks because TextViewDocumentHighlighter.OnHighlightStateChanged
// depends on the fact that equal input state + unchanged line contents produce equal output state.
if (a == b)
return true;
if (a == null || b == null)
return false;
while (!a.IsEmpty && !b.IsEmpty) {
if (a.Peek() != b.Peek())
return false;
a = a.Pop();
b = b.Pop();
if (a == b)
return true;
}
return a.IsEmpty && b.IsEmpty;
}
/// <summary>
/// Is called when the highlighting state at the end of the specified line has changed.
/// </summary>
/// <remarks>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.</remarks>
protected virtual void OnHighlightStateChanged(DocumentLine line, int lineNumber)
{
}
#region Highlighting Engine
SpanStack spanStack;
// local variables from HighlightLineInternal (are member because they are accessed by HighlighLine helper methods)
string lineText;
int lineStartOffset;
int position;
/// <summary>
/// the HighlightedLine where highlighting output is being written to.
/// if this variable is null, nothing is highlighted and only the span state is updated
/// </summary>
HighlightedLine highlightedLine;
void HighlightLineInternal(DocumentLine line)
{
lineStartOffset = line.Offset;
lineText = document.GetText(line.Offset, line.Length);
position = 0;
ResetColorStack();
HighlightingRuleSet currentRuleSet = this.CurrentRuleSet;
Stack<Match[]> storedMatchArrays = new Stack<Match[]>();
Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count);
Match endSpanMatch = null;
while (true) {
for (int i = 0; i < matches.Length; i++) {
if (matches[i] == null || (matches[i].Success && matches[i].Index < position))
matches[i] = currentRuleSet.Spans[i].StartExpression.Match(lineText, position);
}
if (endSpanMatch == null && !spanStack.IsEmpty)
endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position);
Match firstMatch = Minimum(matches, endSpanMatch);
if (firstMatch == null)
break;
HighlightNonSpans(firstMatch.Index);
Debug.Assert(position == firstMatch.Index);
if (firstMatch == endSpanMatch) {
HighlightingSpan poppedSpan = spanStack.Peek();
if (!poppedSpan.SpanColorIncludesEnd)
PopColor(); // pop SpanColor
PushColor(poppedSpan.EndColor);
position = firstMatch.Index + firstMatch.Length;
PopColor(); // pop EndColor
if (poppedSpan.SpanColorIncludesEnd)
PopColor(); // pop SpanColor
spanStack = spanStack.Pop();
currentRuleSet = this.CurrentRuleSet;
//FreeMatchArray(matches);
if (storedMatchArrays.Count > 0) {
matches = storedMatchArrays.Pop();
int index = currentRuleSet.Spans.IndexOf(poppedSpan);
Debug.Assert(index >= 0 && index < matches.Length);
if (matches[index].Index == position) {
throw new InvalidOperationException(
"A highlighting span matched 0 characters, which would cause an endless loop.\n" +
"Change the highlighting definition so that either the start or the end regex matches at least one character.\n" +
"Start regex: " + poppedSpan.StartExpression + "\n" +
"End regex: " + poppedSpan.EndExpression);
}
} else {
matches = AllocateMatchArray(currentRuleSet.Spans.Count);
}
} else {
int index = Array.IndexOf(matches, firstMatch);
Debug.Assert(index >= 0);
HighlightingSpan newSpan = currentRuleSet.Spans[index];
spanStack = spanStack.Push(newSpan);
currentRuleSet = this.CurrentRuleSet;
storedMatchArrays.Push(matches);
matches = AllocateMatchArray(currentRuleSet.Spans.Count);
if (newSpan.SpanColorIncludesStart)
PushColor(newSpan.SpanColor);
PushColor(newSpan.StartColor);
position = firstMatch.Index + firstMatch.Length;
PopColor();
if (!newSpan.SpanColorIncludesStart)
PushColor(newSpan.SpanColor);
}
endSpanMatch = null;
}
HighlightNonSpans(line.Length);
PopAllColors();
}
void HighlightNonSpans(int until)
{
Debug.Assert(position <= until);
if (position == until)
return;
if (highlightedLine != null) {
IList<HighlightingRule> rules = CurrentRuleSet.Rules;
Match[] matches = AllocateMatchArray(rules.Count);
while (true) {
for (int i = 0; i < matches.Length; i++) {
if (matches[i] == null || (matches[i].Success && matches[i].Index < position))
matches[i] = rules[i].Regex.Match(lineText, position, until - position);
}
Match firstMatch = Minimum(matches, null);
if (firstMatch == null)
break;
position = firstMatch.Index;
int ruleIndex = Array.IndexOf(matches, firstMatch);
if (firstMatch.Length == 0) {
throw new InvalidOperationException(
"A highlighting rule matched 0 characters, which would cause an endless loop.\n" +
"Change the highlighting definition so that the rule matches at least one character.\n" +
"Regex: " + rules[ruleIndex].Regex);
}
PushColor(rules[ruleIndex].Color);
position = firstMatch.Index + firstMatch.Length;
PopColor();
}
//FreeMatchArray(matches);
}
position = until;
}
static readonly HighlightingRuleSet emptyRuleSet = new HighlightingRuleSet() { Name = "EmptyRuleSet" };
HighlightingRuleSet CurrentRuleSet {
get {
if (spanStack.IsEmpty)
return baseRuleSet;
else
return spanStack.Peek().RuleSet ?? emptyRuleSet;
}
}
#endregion
#region Color Stack Management
Stack<HighlightedSection> highlightedSectionStack;
HighlightedSection lastPoppedSection;
void ResetColorStack()
{
Debug.Assert(position == 0);
lastPoppedSection = null;
if (highlightedLine == null) {
highlightedSectionStack = null;
} else {
highlightedSectionStack = new Stack<HighlightedSection>();
foreach (HighlightingSpan span in spanStack.Reverse()) {
PushColor(span.SpanColor);
}
}
}
void PushColor(HighlightingColor color)
{
if (highlightedLine == null)
return;
if (color == null) {
highlightedSectionStack.Push(null);
} else if (lastPoppedSection != null && lastPoppedSection.Color == color
&& lastPoppedSection.Offset + lastPoppedSection.Length == position + lineStartOffset)
{
highlightedSectionStack.Push(lastPoppedSection);
lastPoppedSection = null;
} else {
HighlightedSection hs = new HighlightedSection {
Offset = position + lineStartOffset,
Color = color
};
highlightedLine.Sections.Add(hs);
highlightedSectionStack.Push(hs);
lastPoppedSection = null;
}
}
void PopColor()
{
if (highlightedLine == null)
return;
HighlightedSection s = highlightedSectionStack.Pop();
if (s != null) {
s.Length = (position + lineStartOffset) - s.Offset;
if (s.Length == 0)
highlightedLine.Sections.Remove(s);
else
lastPoppedSection = s;
}
}
void PopAllColors()
{
if (highlightedSectionStack != null) {
while (highlightedSectionStack.Count > 0)
PopColor();
}
}
#endregion
#region Match helpers
/// <summary>
/// Returns the first match from the array or endSpanMatch.
/// </summary>
static Match Minimum(Match[] arr, Match endSpanMatch)
{
Match min = null;
foreach (Match v in arr) {
if (v.Success && (min == null || v.Index < min.Index))
min = v;
}
if (endSpanMatch != null && endSpanMatch.Success && (min == null || endSpanMatch.Index < min.Index))
return endSpanMatch;
else
return min;
}
static Match[] AllocateMatchArray(int count)
{
if (count == 0)
return Empty<Match>.Array;
else
return new Match[count];
}
#endregion
}
}