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 ICSharpCode.NRefactory.Editor; |
---|
24 | using ICSharpCode.AvalonEdit.Document; |
---|
25 | using ICSharpCode.AvalonEdit.Utils; |
---|
26 | using SpanStack = ICSharpCode.AvalonEdit.Utils.ImmutableStack<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>; |
---|
27 | |
---|
28 | namespace ICSharpCode.AvalonEdit.Highlighting |
---|
29 | { |
---|
30 | /// <summary> |
---|
31 | /// This class can syntax-highlight a document. |
---|
32 | /// It automatically manages invalidating the highlighting when the document changes. |
---|
33 | /// </summary> |
---|
34 | public class DocumentHighlighter : ILineTracker, IHighlighter |
---|
35 | { |
---|
36 | /// <summary> |
---|
37 | /// Stores the span state at the end of each line. |
---|
38 | /// storedSpanStacks[0] = state at beginning of document |
---|
39 | /// storedSpanStacks[i] = state after line i |
---|
40 | /// </summary> |
---|
41 | readonly CompressingTreeList<SpanStack> storedSpanStacks = new CompressingTreeList<SpanStack>(object.ReferenceEquals); |
---|
42 | readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b); |
---|
43 | readonly IDocument document; |
---|
44 | readonly IHighlightingDefinition definition; |
---|
45 | readonly HighlightingEngine engine; |
---|
46 | readonly WeakLineTracker weakLineTracker; |
---|
47 | bool isHighlighting; |
---|
48 | bool isInHighlightingGroup; |
---|
49 | bool isDisposed; |
---|
50 | |
---|
51 | /// <summary> |
---|
52 | /// Gets the document that this DocumentHighlighter is highlighting. |
---|
53 | /// </summary> |
---|
54 | public IDocument Document { |
---|
55 | get { return document; } |
---|
56 | } |
---|
57 | |
---|
58 | /// <summary> |
---|
59 | /// Creates a new DocumentHighlighter instance. |
---|
60 | /// </summary> |
---|
61 | public DocumentHighlighter(TextDocument document, IHighlightingDefinition definition) |
---|
62 | { |
---|
63 | if (document == null) |
---|
64 | throw new ArgumentNullException("document"); |
---|
65 | if (definition == null) |
---|
66 | throw new ArgumentNullException("definition"); |
---|
67 | this.document = document; |
---|
68 | this.definition = definition; |
---|
69 | this.engine = new HighlightingEngine(definition.MainRuleSet); |
---|
70 | document.VerifyAccess(); |
---|
71 | weakLineTracker = WeakLineTracker.Register(document, this); |
---|
72 | InvalidateHighlighting(); |
---|
73 | } |
---|
74 | |
---|
75 | #if NREFACTORY |
---|
76 | /// <summary> |
---|
77 | /// Creates a new DocumentHighlighter instance. |
---|
78 | /// </summary> |
---|
79 | public DocumentHighlighter(ReadOnlyDocument document, IHighlightingDefinition definition) |
---|
80 | { |
---|
81 | if (document == null) |
---|
82 | throw new ArgumentNullException("document"); |
---|
83 | if (definition == null) |
---|
84 | throw new ArgumentNullException("definition"); |
---|
85 | this.document = document; |
---|
86 | this.definition = definition; |
---|
87 | this.engine = new HighlightingEngine(definition.MainRuleSet); |
---|
88 | InvalidateHighlighting(); |
---|
89 | } |
---|
90 | #endif |
---|
91 | |
---|
92 | /// <summary> |
---|
93 | /// Disposes the document highlighter. |
---|
94 | /// </summary> |
---|
95 | public void Dispose() |
---|
96 | { |
---|
97 | if (weakLineTracker != null) |
---|
98 | weakLineTracker.Deregister(); |
---|
99 | isDisposed = true; |
---|
100 | } |
---|
101 | |
---|
102 | void ILineTracker.BeforeRemoveLine(DocumentLine line) |
---|
103 | { |
---|
104 | CheckIsHighlighting(); |
---|
105 | int number = line.LineNumber; |
---|
106 | storedSpanStacks.RemoveAt(number); |
---|
107 | isValid.RemoveAt(number); |
---|
108 | if (number < isValid.Count) { |
---|
109 | isValid[number] = false; |
---|
110 | if (number < firstInvalidLine) |
---|
111 | firstInvalidLine = number; |
---|
112 | } |
---|
113 | } |
---|
114 | |
---|
115 | void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength) |
---|
116 | { |
---|
117 | CheckIsHighlighting(); |
---|
118 | int number = line.LineNumber; |
---|
119 | isValid[number] = false; |
---|
120 | if (number < firstInvalidLine) |
---|
121 | firstInvalidLine = number; |
---|
122 | } |
---|
123 | |
---|
124 | void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) |
---|
125 | { |
---|
126 | CheckIsHighlighting(); |
---|
127 | Debug.Assert(insertionPos.LineNumber + 1 == newLine.LineNumber); |
---|
128 | int lineNumber = newLine.LineNumber; |
---|
129 | storedSpanStacks.Insert(lineNumber, null); |
---|
130 | isValid.Insert(lineNumber, false); |
---|
131 | if (lineNumber < firstInvalidLine) |
---|
132 | firstInvalidLine = lineNumber; |
---|
133 | } |
---|
134 | |
---|
135 | void ILineTracker.RebuildDocument() |
---|
136 | { |
---|
137 | InvalidateHighlighting(); |
---|
138 | } |
---|
139 | |
---|
140 | void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) |
---|
141 | { |
---|
142 | } |
---|
143 | |
---|
144 | ImmutableStack<HighlightingSpan> initialSpanStack = SpanStack.Empty; |
---|
145 | |
---|
146 | /// <summary> |
---|
147 | /// Gets/sets the the initial span stack of the document. Default value is <see cref="SpanStack.Empty" />. |
---|
148 | /// </summary> |
---|
149 | public ImmutableStack<HighlightingSpan> InitialSpanStack { |
---|
150 | get { return initialSpanStack; } |
---|
151 | set { |
---|
152 | initialSpanStack = value ?? SpanStack.Empty; |
---|
153 | InvalidateHighlighting(); |
---|
154 | } |
---|
155 | } |
---|
156 | |
---|
157 | /// <summary> |
---|
158 | /// Invalidates all stored highlighting info. |
---|
159 | /// When the document changes, the highlighting is invalidated automatically, this method |
---|
160 | /// needs to be called only when there are changes to the highlighting rule set. |
---|
161 | /// </summary> |
---|
162 | public void InvalidateHighlighting() |
---|
163 | { |
---|
164 | CheckIsHighlighting(); |
---|
165 | storedSpanStacks.Clear(); |
---|
166 | storedSpanStacks.Add(initialSpanStack); |
---|
167 | storedSpanStacks.InsertRange(1, document.LineCount, null); |
---|
168 | isValid.Clear(); |
---|
169 | isValid.Add(true); |
---|
170 | isValid.InsertRange(1, document.LineCount, false); |
---|
171 | firstInvalidLine = 1; |
---|
172 | } |
---|
173 | |
---|
174 | int firstInvalidLine; |
---|
175 | |
---|
176 | /// <inheritdoc/> |
---|
177 | public HighlightedLine HighlightLine(int lineNumber) |
---|
178 | { |
---|
179 | ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 1, document.LineCount); |
---|
180 | CheckIsHighlighting(); |
---|
181 | isHighlighting = true; |
---|
182 | try { |
---|
183 | HighlightUpTo(lineNumber - 1); |
---|
184 | IDocumentLine line = document.GetLineByNumber(lineNumber); |
---|
185 | HighlightedLine result = engine.HighlightLine(document, line); |
---|
186 | UpdateTreeList(lineNumber); |
---|
187 | return result; |
---|
188 | } finally { |
---|
189 | isHighlighting = false; |
---|
190 | } |
---|
191 | } |
---|
192 | |
---|
193 | /// <summary> |
---|
194 | /// Gets the span stack at the end of the specified line. |
---|
195 | /// -> GetSpanStack(1) returns the spans at the start of the second line. |
---|
196 | /// </summary> |
---|
197 | /// <remarks> |
---|
198 | /// GetSpanStack(0) is valid and will return <see cref="InitialSpanStack"/>. |
---|
199 | /// The elements are returned in inside-out order (first element of result enumerable is the color of the innermost span). |
---|
200 | /// </remarks> |
---|
201 | public SpanStack GetSpanStack(int lineNumber) |
---|
202 | { |
---|
203 | ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); |
---|
204 | if (firstInvalidLine <= lineNumber) { |
---|
205 | UpdateHighlightingState(lineNumber); |
---|
206 | } |
---|
207 | return storedSpanStacks[lineNumber]; |
---|
208 | } |
---|
209 | |
---|
210 | /// <inheritdoc/> |
---|
211 | public IEnumerable<HighlightingColor> GetColorStack(int lineNumber) |
---|
212 | { |
---|
213 | return GetSpanStack(lineNumber).Select(s => s.SpanColor).Where(s => s != null); |
---|
214 | } |
---|
215 | |
---|
216 | void CheckIsHighlighting() |
---|
217 | { |
---|
218 | if (isDisposed) { |
---|
219 | throw new ObjectDisposedException("DocumentHighlighter"); |
---|
220 | } |
---|
221 | if (isHighlighting) { |
---|
222 | throw new InvalidOperationException("Invalid call - a highlighting operation is currently running."); |
---|
223 | } |
---|
224 | } |
---|
225 | |
---|
226 | /// <inheritdoc/> |
---|
227 | public void UpdateHighlightingState(int lineNumber) |
---|
228 | { |
---|
229 | CheckIsHighlighting(); |
---|
230 | isHighlighting = true; |
---|
231 | try { |
---|
232 | HighlightUpTo(lineNumber); |
---|
233 | } finally { |
---|
234 | isHighlighting = false; |
---|
235 | } |
---|
236 | } |
---|
237 | |
---|
238 | /// <summary> |
---|
239 | /// Sets the engine's CurrentSpanStack to the end of the target line. |
---|
240 | /// Updates the span stack for all lines up to (and including) the target line, if necessary. |
---|
241 | /// </summary> |
---|
242 | void HighlightUpTo(int targetLineNumber) |
---|
243 | { |
---|
244 | for (int currentLine = 0; currentLine <= targetLineNumber; currentLine++) { |
---|
245 | if (firstInvalidLine > currentLine) { |
---|
246 | // (this branch is always taken on the first loop iteration, as firstInvalidLine > 0) |
---|
247 | |
---|
248 | if (firstInvalidLine <= targetLineNumber) { |
---|
249 | // Skip valid lines to next invalid line: |
---|
250 | engine.CurrentSpanStack = storedSpanStacks[firstInvalidLine - 1]; |
---|
251 | currentLine = firstInvalidLine; |
---|
252 | } else { |
---|
253 | // Skip valid lines to target line: |
---|
254 | engine.CurrentSpanStack = storedSpanStacks[targetLineNumber]; |
---|
255 | break; |
---|
256 | } |
---|
257 | } |
---|
258 | Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[currentLine - 1])); |
---|
259 | engine.ScanLine(document, document.GetLineByNumber(currentLine)); |
---|
260 | UpdateTreeList(currentLine); |
---|
261 | } |
---|
262 | Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[targetLineNumber])); |
---|
263 | } |
---|
264 | |
---|
265 | void UpdateTreeList(int lineNumber) |
---|
266 | { |
---|
267 | if (!EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[lineNumber])) { |
---|
268 | isValid[lineNumber] = true; |
---|
269 | //Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack); |
---|
270 | storedSpanStacks[lineNumber] = engine.CurrentSpanStack; |
---|
271 | if (lineNumber + 1 < isValid.Count) { |
---|
272 | isValid[lineNumber + 1] = false; |
---|
273 | firstInvalidLine = lineNumber + 1; |
---|
274 | } else { |
---|
275 | firstInvalidLine = int.MaxValue; |
---|
276 | } |
---|
277 | if (lineNumber + 1 < document.LineCount) |
---|
278 | OnHighlightStateChanged(lineNumber + 1, lineNumber + 1); |
---|
279 | } else if (firstInvalidLine == lineNumber) { |
---|
280 | isValid[lineNumber] = true; |
---|
281 | firstInvalidLine = isValid.IndexOf(false); |
---|
282 | if (firstInvalidLine < 0) |
---|
283 | firstInvalidLine = int.MaxValue; |
---|
284 | } |
---|
285 | } |
---|
286 | |
---|
287 | static bool EqualSpanStacks(SpanStack a, SpanStack b) |
---|
288 | { |
---|
289 | // We must use value equality between the stacks because HighlightingColorizer.OnHighlightStateChanged |
---|
290 | // depends on the fact that equal input state + unchanged line contents produce equal output state. |
---|
291 | if (a == b) |
---|
292 | return true; |
---|
293 | if (a == null || b == null) |
---|
294 | return false; |
---|
295 | while (!a.IsEmpty && !b.IsEmpty) { |
---|
296 | if (a.Peek() != b.Peek()) |
---|
297 | return false; |
---|
298 | a = a.Pop(); |
---|
299 | b = b.Pop(); |
---|
300 | if (a == b) |
---|
301 | return true; |
---|
302 | } |
---|
303 | return a.IsEmpty && b.IsEmpty; |
---|
304 | } |
---|
305 | |
---|
306 | /// <inheritdoc/> |
---|
307 | public event HighlightingStateChangedEventHandler HighlightingStateChanged; |
---|
308 | |
---|
309 | /// <summary> |
---|
310 | /// Is called when the highlighting state at the end of the specified line has changed. |
---|
311 | /// </summary> |
---|
312 | /// <remarks>This callback must not call HighlightLine or InvalidateHighlighting. |
---|
313 | /// It may call GetSpanStack, but only for the changed line and lines above. |
---|
314 | /// This method must not modify the document.</remarks> |
---|
315 | protected virtual void OnHighlightStateChanged(int fromLineNumber, int toLineNumber) |
---|
316 | { |
---|
317 | if (HighlightingStateChanged != null) |
---|
318 | HighlightingStateChanged(fromLineNumber, toLineNumber); |
---|
319 | } |
---|
320 | |
---|
321 | /// <inheritdoc/> |
---|
322 | public HighlightingColor DefaultTextColor { |
---|
323 | get { return null; } |
---|
324 | } |
---|
325 | |
---|
326 | /// <inheritdoc/> |
---|
327 | public void BeginHighlighting() |
---|
328 | { |
---|
329 | if (isInHighlightingGroup) |
---|
330 | throw new InvalidOperationException("Highlighting group is already open"); |
---|
331 | isInHighlightingGroup = true; |
---|
332 | } |
---|
333 | |
---|
334 | /// <inheritdoc/> |
---|
335 | public void EndHighlighting() |
---|
336 | { |
---|
337 | if (!isInHighlightingGroup) |
---|
338 | throw new InvalidOperationException("Highlighting group is not open"); |
---|
339 | isInHighlightingGroup = false; |
---|
340 | } |
---|
341 | |
---|
342 | /// <inheritdoc/> |
---|
343 | public HighlightingColor GetNamedColor(string name) |
---|
344 | { |
---|
345 | return definition.GetNamedColor(name); |
---|
346 | } |
---|
347 | } |
---|
348 | } |
---|