469 lines
15 KiB
C#
469 lines
15 KiB
C#
|
|
// 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
|
|||
|
|
}
|
|||
|
|
}
|