Move regex-based highlighting engine from DocumentHighlighter into its own new class (HighlightingEngine).

DocumentHighlighter now only is only responsible for maintaining the highlighting state (span stacks at line boundaries).
This commit is contained in:
Daniel Grunwald 2013-03-22 18:53:10 +01:00
Родитель 55ea1e5804
Коммит 44b60b07f0
4 изменённых файлов: 348 добавлений и 242 удалений

Просмотреть файл

@ -29,6 +29,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b);
readonly IDocument document;
readonly IHighlightingDefinition definition;
readonly HighlightingEngine engine;
readonly WeakLineTracker weakLineTracker;
bool isHighlighting;
bool isInHighlightingGroup;
@ -52,6 +53,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
throw new ArgumentNullException("definition");
this.document = document;
this.definition = definition;
this.engine = new HighlightingEngine(definition.MainRuleSet);
document.VerifyAccess();
weakLineTracker = WeakLineTracker.Register(document, this);
InvalidateHighlighting();
@ -68,6 +70,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
throw new ArgumentNullException("definition");
this.document = document;
this.definition = definition;
this.engine = new HighlightingEngine(definition.MainRuleSet);
InvalidateHighlighting();
}
@ -131,10 +134,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting
public ImmutableStack<HighlightingSpan> InitialSpanStack {
get { return initialSpanStack; }
set {
if (value == null)
initialSpanStack = SpanStack.Empty;
else
initialSpanStack = value;
initialSpanStack = value ?? SpanStack.Empty;
InvalidateHighlighting();
}
}
@ -167,11 +167,10 @@ namespace ICSharpCode.AvalonEdit.Highlighting
try {
HighlightUpTo(lineNumber - 1);
IDocumentLine line = document.GetLineByNumber(lineNumber);
highlightedLine = new HighlightedLine(document, line);
HighlightLineAndUpdateTreeList(line, lineNumber);
return highlightedLine;
HighlightedLine result = engine.HighlightLine(document, line);
UpdateTreeList(lineNumber);
return result;
} finally {
highlightedLine = null;
isHighlighting = false;
}
}
@ -221,23 +220,39 @@ namespace ICSharpCode.AvalonEdit.Highlighting
}
}
/// <summary>
/// Sets the engine's CurrentSpanStack to the end of the target line.
/// Updates the span stack for all lines up to (and including) the target line, if necessary.
/// </summary>
void HighlightUpTo(int targetLineNumber)
{
Debug.Assert(highlightedLine == null); // ensure this method is only outside the actual highlighting logic
while (firstInvalidLine <= targetLineNumber) {
HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine);
for (int currentLine = 0; currentLine <= targetLineNumber; currentLine++) {
if (firstInvalidLine > currentLine) {
// (this branch is always taken on the first loop iteration, as firstInvalidLine > 0)
if (firstInvalidLine <= targetLineNumber) {
// Skip valid lines to next invalid line:
engine.CurrentSpanStack = storedSpanStacks[firstInvalidLine - 1];
currentLine = firstInvalidLine;
} else {
// Skip valid lines to target line:
engine.CurrentSpanStack = storedSpanStacks[targetLineNumber];
break;
}
}
Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[currentLine - 1]));
engine.ScanLine(document, document.GetLineByNumber(currentLine));
UpdateTreeList(currentLine);
}
Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[targetLineNumber]));
}
void HighlightLineAndUpdateTreeList(IDocumentLine line, int lineNumber)
void UpdateTreeList(int lineNumber)
{
//Debug.WriteLine("Highlight line " + lineNumber + (highlightedLine != null ? "" : " (span stack only)"));
spanStack = storedSpanStacks[lineNumber - 1];
HighlightLineInternal(line);
if (!EqualSpanStacks(spanStack, storedSpanStacks[lineNumber])) {
if (!EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[lineNumber])) {
isValid[lineNumber] = true;
//Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack);
storedSpanStacks[lineNumber] = spanStack;
storedSpanStacks[lineNumber] = engine.CurrentSpanStack;
if (lineNumber + 1 < isValid.Count) {
isValid[lineNumber + 1] = false;
firstInvalidLine = lineNumber + 1;
@ -288,231 +303,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting
HighlightingStateChanged(fromLineNumber, toLineNumber);
}
#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(IDocumentLine line)
{
lineStartOffset = line.Offset;
lineText = document.GetText(lineStartOffset, 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 definition.MainRuleSet;
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
/// <inheritdoc/>
public HighlightingColor DefaultTextColor {
get { return null; }

Просмотреть файл

@ -0,0 +1,303 @@
// 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.NRefactory.Editor;
using ICSharpCode.AvalonEdit.Utils;
using SpanStack = ICSharpCode.NRefactory.Utils.ImmutableStack<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// Regex-based highlighting engine.
/// </summary>
public class HighlightingEngine
{
readonly HighlightingRuleSet mainRuleSet;
SpanStack spanStack = SpanStack.Empty;
public HighlightingEngine(HighlightingRuleSet mainRuleSet)
{
if (mainRuleSet == null)
throw new ArgumentNullException("mainRuleSet");
this.mainRuleSet = mainRuleSet;
}
/// <summary>
/// Gets/sets the current span stack.
/// </summary>
public SpanStack CurrentSpanStack {
get { return spanStack; }
set {
spanStack = value ?? SpanStack.Empty;
}
}
#region Highlighting Engine
// 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;
/// <summary>
/// Highlights the specified line in the specified document.
///
/// Before calling this method, <see cref="CurrentSpanStack"/> must be set to the proper
/// state for the beginning of this line. After highlighting has completed,
/// <see cref="CurrentSpanStack"/> will be updated to represent the state after the line.
/// </summary>
public HighlightedLine HighlightLine(IDocument document, IDocumentLine line)
{
this.lineStartOffset = line.Offset;
this.lineText = document.GetText(line);
try {
this.highlightedLine = new HighlightedLine(document, line);
HighlightLineInternal();
return this.highlightedLine;
} finally {
this.highlightedLine = null;
this.lineText = null;
this.lineStartOffset = 0;
}
}
/// <summary>
/// Updates <see cref="CurrentSpanStack"/> for the specified line in the specified document.
///
/// Before calling this method, <see cref="CurrentSpanStack"/> must be set to the proper
/// state for the beginning of this line. After highlighting has completed,
/// <see cref="CurrentSpanStack"/> will be updated to represent the state after the line.
/// </summary>
public void ScanLine(IDocument document, IDocumentLine line)
{
//this.lineStartOffset = line.Offset; not necessary for scanning
this.lineText = document.GetText(line);
try {
Debug.Assert(highlightedLine == null);
HighlightLineInternal();
} finally {
this.lineText = null;
}
}
void HighlightLineInternal()
{
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(lineText.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 mainRuleSet;
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
}
}

Просмотреть файл

@ -193,6 +193,7 @@
<Compile Include="Highlighting\HighlightingColorizer.cs" />
<Compile Include="Highlighting\HighlightingDefinitionInvalidException.cs" />
<Compile Include="Highlighting\HighlightingDefinitionTypeConverter.cs" />
<Compile Include="Highlighting\HighlightingEngine.cs" />
<Compile Include="Highlighting\HighlightingManager.cs" />
<Compile Include="Highlighting\HtmlClipboard.cs" />
<Compile Include="Highlighting\IHighlighter.cs" />

Просмотреть файл

@ -110,6 +110,18 @@ namespace ICSharpCode.AvalonEdit.Utils
readonly Func<T, T, bool> comparisonFunc;
Node root;
/// <summary>
/// Creates a new CompressingTreeList instance.
/// </summary>
/// <param name="comparisonFunc">A function that checks two values for equality. If this
/// function returns true, a single node may be used to store the two values.</param>
public CompressingTreeList(IEqualityComparer<T> equalityComparer)
{
if (equalityComparer == null)
throw new ArgumentNullException("equalityComparer");
this.comparisonFunc = equalityComparer.Equals;
}
/// <summary>
/// Creates a new CompressingTreeList instance.
/// </summary>