aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs
diff options
context:
space:
mode:
authorVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
committerVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
commitfc8a05358a92cc3c77c5f1e30d536807ef0614fd (patch)
treec65f696ebd60f3790145721307c255e5a339923f /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs
parentb4a71931ea52636c6b36376aa9d71697ccf73524 (diff)
downloadTango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.tar.gz
Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.zip
were added scripting projects
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs')
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs468
1 files changed, 468 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs
new file mode 100644
index 000000000..393c0a930
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Highlighting/DocumentHighlighter.cs
@@ -0,0 +1,468 @@
+// 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 Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+using SpanStack = Tango.Scripting.Editors.Utils.ImmutableStack<Tango.Scripting.Editors.Highlighting.HighlightingSpan>;
+
+namespace Tango.Scripting.Editors.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
+ }
+}