1 | // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team |
---|
2 | // |
---|
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this |
---|
4 | // software and associated documentation files (the "Software"), to deal in the Software |
---|
5 | // without restriction, including without limitation the rights to use, copy, modify, merge, |
---|
6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
---|
7 | // to whom the Software is furnished to do so, subject to the following conditions: |
---|
8 | // |
---|
9 | // The above copyright notice and this permission notice shall be included in all copies or |
---|
10 | // substantial portions of the Software. |
---|
11 | // |
---|
12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
---|
13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
---|
14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
---|
15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
---|
16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
---|
17 | // DEALINGS IN THE SOFTWARE. |
---|
18 | |
---|
19 | using System; |
---|
20 | using System.Collections.Generic; |
---|
21 | using System.Diagnostics; |
---|
22 | using System.Linq; |
---|
23 | using System.Text.RegularExpressions; |
---|
24 | #if NREFACTORY |
---|
25 | using ICSharpCode.NRefactory.Editor; |
---|
26 | #else |
---|
27 | using ICSharpCode.AvalonEdit.Document; |
---|
28 | #endif |
---|
29 | using ICSharpCode.AvalonEdit.Utils; |
---|
30 | using SpanStack = ICSharpCode.AvalonEdit.Utils.ImmutableStack<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>; |
---|
31 | |
---|
32 | namespace ICSharpCode.AvalonEdit.Highlighting |
---|
33 | { |
---|
34 | /// <summary> |
---|
35 | /// Regex-based highlighting engine. |
---|
36 | /// </summary> |
---|
37 | public class HighlightingEngine |
---|
38 | { |
---|
39 | readonly HighlightingRuleSet mainRuleSet; |
---|
40 | SpanStack spanStack = SpanStack.Empty; |
---|
41 | |
---|
42 | /// <summary> |
---|
43 | /// Creates a new HighlightingEngine instance. |
---|
44 | /// </summary> |
---|
45 | public HighlightingEngine(HighlightingRuleSet mainRuleSet) |
---|
46 | { |
---|
47 | if (mainRuleSet == null) |
---|
48 | throw new ArgumentNullException("mainRuleSet"); |
---|
49 | this.mainRuleSet = mainRuleSet; |
---|
50 | } |
---|
51 | |
---|
52 | /// <summary> |
---|
53 | /// Gets/sets the current span stack. |
---|
54 | /// </summary> |
---|
55 | public SpanStack CurrentSpanStack { |
---|
56 | get { return spanStack; } |
---|
57 | set { |
---|
58 | spanStack = value ?? SpanStack.Empty; |
---|
59 | } |
---|
60 | } |
---|
61 | |
---|
62 | #region Highlighting Engine |
---|
63 | |
---|
64 | // local variables from HighlightLineInternal (are member because they are accessed by HighlighLine helper methods) |
---|
65 | string lineText; |
---|
66 | int lineStartOffset; |
---|
67 | int position; |
---|
68 | |
---|
69 | /// <summary> |
---|
70 | /// the HighlightedLine where highlighting output is being written to. |
---|
71 | /// if this variable is null, nothing is highlighted and only the span state is updated |
---|
72 | /// </summary> |
---|
73 | HighlightedLine highlightedLine; |
---|
74 | |
---|
75 | /// <summary> |
---|
76 | /// Highlights the specified line in the specified document. |
---|
77 | /// |
---|
78 | /// Before calling this method, <see cref="CurrentSpanStack"/> must be set to the proper |
---|
79 | /// state for the beginning of this line. After highlighting has completed, |
---|
80 | /// <see cref="CurrentSpanStack"/> will be updated to represent the state after the line. |
---|
81 | /// </summary> |
---|
82 | public HighlightedLine HighlightLine(IDocument document, IDocumentLine line) |
---|
83 | { |
---|
84 | this.lineStartOffset = line.Offset; |
---|
85 | this.lineText = document.GetText(line); |
---|
86 | try { |
---|
87 | this.highlightedLine = new HighlightedLine(document, line); |
---|
88 | HighlightLineInternal(); |
---|
89 | return this.highlightedLine; |
---|
90 | } finally { |
---|
91 | this.highlightedLine = null; |
---|
92 | this.lineText = null; |
---|
93 | this.lineStartOffset = 0; |
---|
94 | } |
---|
95 | } |
---|
96 | |
---|
97 | /// <summary> |
---|
98 | /// Updates <see cref="CurrentSpanStack"/> for the specified line in the specified document. |
---|
99 | /// |
---|
100 | /// Before calling this method, <see cref="CurrentSpanStack"/> must be set to the proper |
---|
101 | /// state for the beginning of this line. After highlighting has completed, |
---|
102 | /// <see cref="CurrentSpanStack"/> will be updated to represent the state after the line. |
---|
103 | /// </summary> |
---|
104 | public void ScanLine(IDocument document, IDocumentLine line) |
---|
105 | { |
---|
106 | //this.lineStartOffset = line.Offset; not necessary for scanning |
---|
107 | this.lineText = document.GetText(line); |
---|
108 | try { |
---|
109 | Debug.Assert(highlightedLine == null); |
---|
110 | HighlightLineInternal(); |
---|
111 | } finally { |
---|
112 | this.lineText = null; |
---|
113 | } |
---|
114 | } |
---|
115 | |
---|
116 | void HighlightLineInternal() |
---|
117 | { |
---|
118 | position = 0; |
---|
119 | ResetColorStack(); |
---|
120 | HighlightingRuleSet currentRuleSet = this.CurrentRuleSet; |
---|
121 | Stack<Match[]> storedMatchArrays = new Stack<Match[]>(); |
---|
122 | Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count); |
---|
123 | Match endSpanMatch = null; |
---|
124 | |
---|
125 | while (true) { |
---|
126 | for (int i = 0; i < matches.Length; i++) { |
---|
127 | if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) |
---|
128 | matches[i] = currentRuleSet.Spans[i].StartExpression.Match(lineText, position); |
---|
129 | } |
---|
130 | if (endSpanMatch == null && !spanStack.IsEmpty) |
---|
131 | endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position); |
---|
132 | |
---|
133 | Match firstMatch = Minimum(matches, endSpanMatch); |
---|
134 | if (firstMatch == null) |
---|
135 | break; |
---|
136 | |
---|
137 | HighlightNonSpans(firstMatch.Index); |
---|
138 | |
---|
139 | Debug.Assert(position == firstMatch.Index); |
---|
140 | |
---|
141 | if (firstMatch == endSpanMatch) { |
---|
142 | HighlightingSpan poppedSpan = spanStack.Peek(); |
---|
143 | if (!poppedSpan.SpanColorIncludesEnd) |
---|
144 | PopColor(); // pop SpanColor |
---|
145 | PushColor(poppedSpan.EndColor); |
---|
146 | position = firstMatch.Index + firstMatch.Length; |
---|
147 | PopColor(); // pop EndColor |
---|
148 | if (poppedSpan.SpanColorIncludesEnd) |
---|
149 | PopColor(); // pop SpanColor |
---|
150 | spanStack = spanStack.Pop(); |
---|
151 | currentRuleSet = this.CurrentRuleSet; |
---|
152 | //FreeMatchArray(matches); |
---|
153 | if (storedMatchArrays.Count > 0) { |
---|
154 | matches = storedMatchArrays.Pop(); |
---|
155 | int index = currentRuleSet.Spans.IndexOf(poppedSpan); |
---|
156 | Debug.Assert(index >= 0 && index < matches.Length); |
---|
157 | if (matches[index].Index == position) { |
---|
158 | throw new InvalidOperationException( |
---|
159 | "A highlighting span matched 0 characters, which would cause an endless loop.\n" + |
---|
160 | "Change the highlighting definition so that either the start or the end regex matches at least one character.\n" + |
---|
161 | "Start regex: " + poppedSpan.StartExpression + "\n" + |
---|
162 | "End regex: " + poppedSpan.EndExpression); |
---|
163 | } |
---|
164 | } else { |
---|
165 | matches = AllocateMatchArray(currentRuleSet.Spans.Count); |
---|
166 | } |
---|
167 | } else { |
---|
168 | int index = Array.IndexOf(matches, firstMatch); |
---|
169 | Debug.Assert(index >= 0); |
---|
170 | HighlightingSpan newSpan = currentRuleSet.Spans[index]; |
---|
171 | spanStack = spanStack.Push(newSpan); |
---|
172 | currentRuleSet = this.CurrentRuleSet; |
---|
173 | storedMatchArrays.Push(matches); |
---|
174 | matches = AllocateMatchArray(currentRuleSet.Spans.Count); |
---|
175 | if (newSpan.SpanColorIncludesStart) |
---|
176 | PushColor(newSpan.SpanColor); |
---|
177 | PushColor(newSpan.StartColor); |
---|
178 | position = firstMatch.Index + firstMatch.Length; |
---|
179 | PopColor(); |
---|
180 | if (!newSpan.SpanColorIncludesStart) |
---|
181 | PushColor(newSpan.SpanColor); |
---|
182 | } |
---|
183 | endSpanMatch = null; |
---|
184 | } |
---|
185 | HighlightNonSpans(lineText.Length); |
---|
186 | |
---|
187 | PopAllColors(); |
---|
188 | } |
---|
189 | |
---|
190 | void HighlightNonSpans(int until) |
---|
191 | { |
---|
192 | Debug.Assert(position <= until); |
---|
193 | if (position == until) |
---|
194 | return; |
---|
195 | if (highlightedLine != null) { |
---|
196 | IList<HighlightingRule> rules = CurrentRuleSet.Rules; |
---|
197 | Match[] matches = AllocateMatchArray(rules.Count); |
---|
198 | while (true) { |
---|
199 | for (int i = 0; i < matches.Length; i++) { |
---|
200 | if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) |
---|
201 | matches[i] = rules[i].Regex.Match(lineText, position, until - position); |
---|
202 | } |
---|
203 | Match firstMatch = Minimum(matches, null); |
---|
204 | if (firstMatch == null) |
---|
205 | break; |
---|
206 | |
---|
207 | position = firstMatch.Index; |
---|
208 | int ruleIndex = Array.IndexOf(matches, firstMatch); |
---|
209 | if (firstMatch.Length == 0) { |
---|
210 | throw new InvalidOperationException( |
---|
211 | "A highlighting rule matched 0 characters, which would cause an endless loop.\n" + |
---|
212 | "Change the highlighting definition so that the rule matches at least one character.\n" + |
---|
213 | "Regex: " + rules[ruleIndex].Regex); |
---|
214 | } |
---|
215 | PushColor(rules[ruleIndex].Color); |
---|
216 | position = firstMatch.Index + firstMatch.Length; |
---|
217 | PopColor(); |
---|
218 | } |
---|
219 | //FreeMatchArray(matches); |
---|
220 | } |
---|
221 | position = until; |
---|
222 | } |
---|
223 | |
---|
224 | static readonly HighlightingRuleSet emptyRuleSet = new HighlightingRuleSet() { Name = "EmptyRuleSet" }; |
---|
225 | |
---|
226 | HighlightingRuleSet CurrentRuleSet { |
---|
227 | get { |
---|
228 | if (spanStack.IsEmpty) |
---|
229 | return mainRuleSet; |
---|
230 | else |
---|
231 | return spanStack.Peek().RuleSet ?? emptyRuleSet; |
---|
232 | } |
---|
233 | } |
---|
234 | #endregion |
---|
235 | |
---|
236 | #region Color Stack Management |
---|
237 | Stack<HighlightedSection> highlightedSectionStack; |
---|
238 | HighlightedSection lastPoppedSection; |
---|
239 | |
---|
240 | void ResetColorStack() |
---|
241 | { |
---|
242 | Debug.Assert(position == 0); |
---|
243 | lastPoppedSection = null; |
---|
244 | if (highlightedLine == null) { |
---|
245 | highlightedSectionStack = null; |
---|
246 | } else { |
---|
247 | highlightedSectionStack = new Stack<HighlightedSection>(); |
---|
248 | foreach (HighlightingSpan span in spanStack.Reverse()) { |
---|
249 | PushColor(span.SpanColor); |
---|
250 | } |
---|
251 | } |
---|
252 | } |
---|
253 | |
---|
254 | void PushColor(HighlightingColor color) |
---|
255 | { |
---|
256 | if (highlightedLine == null) |
---|
257 | return; |
---|
258 | if (color == null) { |
---|
259 | highlightedSectionStack.Push(null); |
---|
260 | } else if (lastPoppedSection != null && lastPoppedSection.Color == color |
---|
261 | && lastPoppedSection.Offset + lastPoppedSection.Length == position + lineStartOffset) |
---|
262 | { |
---|
263 | highlightedSectionStack.Push(lastPoppedSection); |
---|
264 | lastPoppedSection = null; |
---|
265 | } else { |
---|
266 | HighlightedSection hs = new HighlightedSection { |
---|
267 | Offset = position + lineStartOffset, |
---|
268 | Color = color |
---|
269 | }; |
---|
270 | highlightedLine.Sections.Add(hs); |
---|
271 | highlightedSectionStack.Push(hs); |
---|
272 | lastPoppedSection = null; |
---|
273 | } |
---|
274 | } |
---|
275 | |
---|
276 | void PopColor() |
---|
277 | { |
---|
278 | if (highlightedLine == null) |
---|
279 | return; |
---|
280 | HighlightedSection s = highlightedSectionStack.Pop(); |
---|
281 | if (s != null) { |
---|
282 | s.Length = (position + lineStartOffset) - s.Offset; |
---|
283 | if (s.Length == 0) |
---|
284 | highlightedLine.Sections.Remove(s); |
---|
285 | else |
---|
286 | lastPoppedSection = s; |
---|
287 | } |
---|
288 | } |
---|
289 | |
---|
290 | void PopAllColors() |
---|
291 | { |
---|
292 | if (highlightedSectionStack != null) { |
---|
293 | while (highlightedSectionStack.Count > 0) |
---|
294 | PopColor(); |
---|
295 | } |
---|
296 | } |
---|
297 | #endregion |
---|
298 | |
---|
299 | #region Match helpers |
---|
300 | /// <summary> |
---|
301 | /// Returns the first match from the array or endSpanMatch. |
---|
302 | /// </summary> |
---|
303 | static Match Minimum(Match[] arr, Match endSpanMatch) |
---|
304 | { |
---|
305 | Match min = null; |
---|
306 | foreach (Match v in arr) { |
---|
307 | if (v.Success && (min == null || v.Index < min.Index)) |
---|
308 | min = v; |
---|
309 | } |
---|
310 | if (endSpanMatch != null && endSpanMatch.Success && (min == null || endSpanMatch.Index < min.Index)) |
---|
311 | return endSpanMatch; |
---|
312 | else |
---|
313 | return min; |
---|
314 | } |
---|
315 | |
---|
316 | static Match[] AllocateMatchArray(int count) |
---|
317 | { |
---|
318 | if (count == 0) |
---|
319 | return Empty<Match>.Array; |
---|
320 | else |
---|
321 | return new Match[count]; |
---|
322 | } |
---|
323 | #endregion |
---|
324 | } |
---|
325 | } |
---|