aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingSection.cs
blob: fd27e9b3b85ca22f92bfc28a24571547b9894b2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// 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.Diagnostics;
using System.Text;
using System.Windows.Threading;
using Tango.Scripting.Editors.Document;
using Tango.Scripting.Editors.Rendering;
using Tango.Scripting.Editors.Utils;

namespace Tango.Scripting.Editors.Folding
{
	/// <summary>
	/// A section that can be folded.
	/// </summary>
	public sealed class FoldingSection : TextSegment
	{
		readonly FoldingManager manager;
		bool isFolded;
		internal CollapsedLineSection[] collapsedSections;
		string title;
		
		/// <summary>
		/// Gets/sets if the section is folded.
		/// </summary>
		public bool IsFolded {
			get { return isFolded; }
			set {
				if (isFolded != value) {
					isFolded = value;
					ValidateCollapsedLineSections(); // create/destroy CollapsedLineSection
						manager.Redraw(this);
				}
			}
		}
		
		internal void ValidateCollapsedLineSections()
		{
			if (!isFolded) {
				RemoveCollapsedLineSection();
				return;
			}
			// It is possible that StartOffset/EndOffset get set to invalid values via the property setters in TextSegment,
			// so we coerce those values into the valid range.
			DocumentLine startLine = manager.document.GetLineByOffset(StartOffset.CoerceValue(0, manager.document.TextLength));
			DocumentLine endLine = manager.document.GetLineByOffset(EndOffset.CoerceValue(0, manager.document.TextLength));
			if (startLine == endLine) {
				RemoveCollapsedLineSection();
			} else {
				if (collapsedSections == null)
					collapsedSections = new CollapsedLineSection[manager.textViews.Count];
				// Validate collapsed line sections
				DocumentLine startLinePlusOne = startLine.NextLine;
				for (int i = 0; i < collapsedSections.Length; i++) {
					var collapsedSection = collapsedSections[i];
					if (collapsedSection == null || collapsedSection.Start != startLinePlusOne || collapsedSection.End != endLine) {
						// recreate this collapsed section
						if (collapsedSection != null) {
							Debug.WriteLine("CollapsedLineSection validation - recreate collapsed section from " + startLinePlusOne + " to " + endLine);
							collapsedSection.Uncollapse();
						}
						collapsedSections[i] = manager.textViews[i].CollapseLines(startLinePlusOne, endLine);
					}
				}
			}
		}
		
		/// <inheritdoc/>
		protected override void OnSegmentChanged()
		{
			ValidateCollapsedLineSections();
			base.OnSegmentChanged();
			// don't redraw if the FoldingSection wasn't added to the FoldingManager's collection yet
			if (IsConnectedToCollection)
				manager.Redraw(this);
		}
		
		/// <summary>
		/// Gets/Sets the text used to display the collapsed version of the folding section.
		/// </summary>
		public string Title {
			get {
				return title;
			}
			set {
				if (title != value) {
					title = value;
					if (this.IsFolded)
						manager.Redraw(this);
				}
			}
		}
		
		/// <summary>
		/// Gets the content of the collapsed lines as text.
		/// </summary>
		public string TextContent {
			get {
				return manager.document.GetText(StartOffset, EndOffset - StartOffset);
			}
		}
		
		/// <summary>
		/// Gets the content of the collapsed lines as tooltip text.
		/// </summary>
		[Obsolete]
		public string TooltipText {
			get {
				// This fixes SD-1394:
				// Each line is checked for leading indentation whitespaces. If
				// a line has the same or more indentation than the first line,
				// it is reduced. If a line is less indented than the first line
				// the indentation is removed completely.
				//
				// See the following example:
				// 	line 1
				// 		line 2
				// 			line 3
				//  line 4
				//
				// is reduced to:
				// line 1
				// 	line 2
				// 		line 3
				// line 4
				
				var startLine = manager.document.GetLineByOffset(StartOffset);
				var endLine = manager.document.GetLineByOffset(EndOffset);
				var builder = new StringBuilder();
				
				var current = startLine;
				ISegment startIndent = TextUtilities.GetLeadingWhitespace(manager.document, startLine);
				
				while (current != endLine.NextLine) {
					ISegment currentIndent = TextUtilities.GetLeadingWhitespace(manager.document, current);
					
					if (current == startLine && current == endLine)
						builder.Append(manager.document.GetText(StartOffset, EndOffset - StartOffset));
					else if (current == startLine) {
						if (current.EndOffset - StartOffset > 0)
							builder.AppendLine(manager.document.GetText(StartOffset, current.EndOffset - StartOffset).TrimStart());
					} else if (current == endLine) {
						if (startIndent.Length <= currentIndent.Length)
							builder.Append(manager.document.GetText(current.Offset + startIndent.Length, EndOffset - current.Offset - startIndent.Length));
						else
							builder.Append(manager.document.GetText(current.Offset + currentIndent.Length, EndOffset - current.Offset - currentIndent.Length));
					} else {
						if (startIndent.Length <= currentIndent.Length)
							builder.AppendLine(manager.document.GetText(current.Offset + startIndent.Length, current.Length - startIndent.Length));
						else
							builder.AppendLine(manager.document.GetText(current.Offset + currentIndent.Length, current.Length - currentIndent.Length));
					}
					
					current = current.NextLine;
				}
				
				return builder.ToString();
			}
		}
		
		/// <summary>
		/// Gets/Sets an additional object associated with this folding section.
		/// </summary>
		public object Tag { get; set; }
		
		internal FoldingSection(FoldingManager manager, int startOffset, int endOffset)
		{
			Debug.Assert(manager != null);
			this.manager = manager;
			this.StartOffset = startOffset;
			this.Length = endOffset - startOffset;
		}
		
		void RemoveCollapsedLineSection()
		{
			if (collapsedSections != null) {
				foreach (var collapsedSection in collapsedSections) {
					if (collapsedSection != null && collapsedSection.Start != null)
						collapsedSection.Uncollapse();
				}
				collapsedSections = null;
			}
		}
	}
}
s="n">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 } }