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.Diagnostics; |
---|
21 | using System.Windows.Media; |
---|
22 | using ICSharpCode.AvalonEdit.Document; |
---|
23 | using ICSharpCode.AvalonEdit.Rendering; |
---|
24 | |
---|
25 | namespace ICSharpCode.AvalonEdit.Highlighting |
---|
26 | { |
---|
27 | /// <summary> |
---|
28 | /// A colorizes that interprets a highlighting rule set and colors the document accordingly. |
---|
29 | /// </summary> |
---|
30 | public class HighlightingColorizer : DocumentColorizingTransformer |
---|
31 | { |
---|
32 | readonly IHighlightingDefinition definition; |
---|
33 | TextView textView; |
---|
34 | IHighlighter highlighter; |
---|
35 | bool isFixedHighlighter; |
---|
36 | |
---|
37 | /// <summary> |
---|
38 | /// Creates a new HighlightingColorizer instance. |
---|
39 | /// </summary> |
---|
40 | /// <param name="definition">The highlighting definition.</param> |
---|
41 | public HighlightingColorizer(IHighlightingDefinition definition) |
---|
42 | { |
---|
43 | if (definition == null) |
---|
44 | throw new ArgumentNullException("definition"); |
---|
45 | this.definition = definition; |
---|
46 | } |
---|
47 | |
---|
48 | /// <summary> |
---|
49 | /// Creates a new HighlightingColorizer instance that uses a fixed highlighter instance. |
---|
50 | /// The colorizer can only be used with text views that show the document for which |
---|
51 | /// the highlighter was created. |
---|
52 | /// </summary> |
---|
53 | /// <param name="highlighter">The highlighter to be used.</param> |
---|
54 | public HighlightingColorizer(IHighlighter highlighter) |
---|
55 | { |
---|
56 | if (highlighter == null) |
---|
57 | throw new ArgumentNullException("highlighter"); |
---|
58 | this.highlighter = highlighter; |
---|
59 | this.isFixedHighlighter = true; |
---|
60 | } |
---|
61 | |
---|
62 | /// <summary> |
---|
63 | /// Creates a new HighlightingColorizer instance. |
---|
64 | /// Derived classes using this constructor must override the <see cref="CreateHighlighter"/> method. |
---|
65 | /// </summary> |
---|
66 | protected HighlightingColorizer() |
---|
67 | { |
---|
68 | } |
---|
69 | |
---|
70 | void textView_DocumentChanged(object sender, EventArgs e) |
---|
71 | { |
---|
72 | TextView textView = (TextView)sender; |
---|
73 | DeregisterServices(textView); |
---|
74 | RegisterServices(textView); |
---|
75 | } |
---|
76 | |
---|
77 | /// <summary> |
---|
78 | /// This method is called when a text view is removed from this HighlightingColorizer, |
---|
79 | /// and also when the TextDocument on any associated text view changes. |
---|
80 | /// </summary> |
---|
81 | protected virtual void DeregisterServices(TextView textView) |
---|
82 | { |
---|
83 | if (highlighter != null) { |
---|
84 | if (isInHighlightingGroup) { |
---|
85 | highlighter.EndHighlighting(); |
---|
86 | isInHighlightingGroup = false; |
---|
87 | } |
---|
88 | highlighter.HighlightingStateChanged -= OnHighlightStateChanged; |
---|
89 | // remove highlighter if it is registered |
---|
90 | if (textView.Services.GetService(typeof(IHighlighter)) == highlighter) |
---|
91 | textView.Services.RemoveService(typeof(IHighlighter)); |
---|
92 | if (!isFixedHighlighter) { |
---|
93 | if (highlighter != null) |
---|
94 | highlighter.Dispose(); |
---|
95 | highlighter = null; |
---|
96 | } |
---|
97 | } |
---|
98 | } |
---|
99 | |
---|
100 | /// <summary> |
---|
101 | /// This method is called when a new text view is added to this HighlightingColorizer, |
---|
102 | /// and also when the TextDocument on any associated text view changes. |
---|
103 | /// </summary> |
---|
104 | protected virtual void RegisterServices(TextView textView) |
---|
105 | { |
---|
106 | if (textView.Document != null) { |
---|
107 | if (!isFixedHighlighter) |
---|
108 | highlighter = textView.Document != null ? CreateHighlighter(textView, textView.Document) : null; |
---|
109 | if (highlighter != null && highlighter.Document == textView.Document) { |
---|
110 | // add service only if it doesn't already exist |
---|
111 | if (textView.Services.GetService(typeof(IHighlighter)) == null) { |
---|
112 | textView.Services.AddService(typeof(IHighlighter), highlighter); |
---|
113 | } |
---|
114 | highlighter.HighlightingStateChanged += OnHighlightStateChanged; |
---|
115 | } |
---|
116 | } |
---|
117 | } |
---|
118 | |
---|
119 | /// <summary> |
---|
120 | /// Creates the IHighlighter instance for the specified text document. |
---|
121 | /// </summary> |
---|
122 | protected virtual IHighlighter CreateHighlighter(TextView textView, TextDocument document) |
---|
123 | { |
---|
124 | if (definition != null) |
---|
125 | return new DocumentHighlighter(document, definition); |
---|
126 | else |
---|
127 | throw new NotSupportedException("Cannot create a highlighter because no IHighlightingDefinition was specified, and the CreateHighlighter() method was not overridden."); |
---|
128 | } |
---|
129 | |
---|
130 | /// <inheritdoc/> |
---|
131 | protected override void OnAddToTextView(TextView textView) |
---|
132 | { |
---|
133 | if (this.textView != null) { |
---|
134 | throw new InvalidOperationException("Cannot use a HighlightingColorizer instance in multiple text views. Please create a separate instance for each text view."); |
---|
135 | } |
---|
136 | base.OnAddToTextView(textView); |
---|
137 | this.textView = textView; |
---|
138 | textView.DocumentChanged += textView_DocumentChanged; |
---|
139 | textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting; |
---|
140 | textView.VisualLinesChanged += textView_VisualLinesChanged; |
---|
141 | RegisterServices(textView); |
---|
142 | } |
---|
143 | |
---|
144 | /// <inheritdoc/> |
---|
145 | protected override void OnRemoveFromTextView(TextView textView) |
---|
146 | { |
---|
147 | DeregisterServices(textView); |
---|
148 | textView.DocumentChanged -= textView_DocumentChanged; |
---|
149 | textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting; |
---|
150 | textView.VisualLinesChanged -= textView_VisualLinesChanged; |
---|
151 | base.OnRemoveFromTextView(textView); |
---|
152 | this.textView = null; |
---|
153 | } |
---|
154 | |
---|
155 | bool isInHighlightingGroup; |
---|
156 | |
---|
157 | void textView_VisualLineConstructionStarting(object sender, VisualLineConstructionStartEventArgs e) |
---|
158 | { |
---|
159 | if (highlighter != null) { |
---|
160 | // Force update of highlighting state up to the position where we start generating visual lines. |
---|
161 | // This is necessary in case the document gets modified above the FirstLineInView so that the highlighting state changes. |
---|
162 | // We need to detect this case and issue a redraw (through OnHighlightStateChanged) |
---|
163 | // before the visual line construction reuses existing lines that were built using the invalid highlighting state. |
---|
164 | lineNumberBeingColorized = e.FirstLineInView.LineNumber - 1; |
---|
165 | if (!isInHighlightingGroup) { |
---|
166 | // avoid opening group twice if there was an exception during the previous visual line construction |
---|
167 | // (not ideal, but better than throwing InvalidOperationException "group already open" |
---|
168 | // without any way of recovering) |
---|
169 | highlighter.BeginHighlighting(); |
---|
170 | isInHighlightingGroup = true; |
---|
171 | } |
---|
172 | highlighter.UpdateHighlightingState(lineNumberBeingColorized); |
---|
173 | lineNumberBeingColorized = 0; |
---|
174 | } |
---|
175 | } |
---|
176 | |
---|
177 | void textView_VisualLinesChanged(object sender, EventArgs e) |
---|
178 | { |
---|
179 | if (highlighter != null && isInHighlightingGroup) { |
---|
180 | highlighter.EndHighlighting(); |
---|
181 | isInHighlightingGroup = false; |
---|
182 | } |
---|
183 | } |
---|
184 | |
---|
185 | DocumentLine lastColorizedLine; |
---|
186 | |
---|
187 | /// <inheritdoc/> |
---|
188 | protected override void Colorize(ITextRunConstructionContext context) |
---|
189 | { |
---|
190 | this.lastColorizedLine = null; |
---|
191 | base.Colorize(context); |
---|
192 | if (this.lastColorizedLine != context.VisualLine.LastDocumentLine) { |
---|
193 | if (highlighter != null) { |
---|
194 | // In some cases, it is possible that we didn't highlight the last document line within the visual line |
---|
195 | // (e.g. when the line ends with a fold marker). |
---|
196 | // But even if we didn't highlight it, we'll have to update the highlighting state for it so that the |
---|
197 | // proof inside TextViewDocumentHighlighter.OnHighlightStateChanged holds. |
---|
198 | lineNumberBeingColorized = context.VisualLine.LastDocumentLine.LineNumber; |
---|
199 | highlighter.UpdateHighlightingState(lineNumberBeingColorized); |
---|
200 | lineNumberBeingColorized = 0; |
---|
201 | } |
---|
202 | } |
---|
203 | this.lastColorizedLine = null; |
---|
204 | } |
---|
205 | |
---|
206 | int lineNumberBeingColorized; |
---|
207 | |
---|
208 | /// <inheritdoc/> |
---|
209 | protected override void ColorizeLine(DocumentLine line) |
---|
210 | { |
---|
211 | if (highlighter != null) { |
---|
212 | lineNumberBeingColorized = line.LineNumber; |
---|
213 | HighlightedLine hl = highlighter.HighlightLine(lineNumberBeingColorized); |
---|
214 | lineNumberBeingColorized = 0; |
---|
215 | foreach (HighlightedSection section in hl.Sections) { |
---|
216 | if (IsEmptyColor(section.Color)) |
---|
217 | continue; |
---|
218 | ChangeLinePart(section.Offset, section.Offset + section.Length, |
---|
219 | visualLineElement => ApplyColorToElement(visualLineElement, section.Color)); |
---|
220 | } |
---|
221 | } |
---|
222 | this.lastColorizedLine = line; |
---|
223 | } |
---|
224 | |
---|
225 | /// <summary> |
---|
226 | /// Gets whether the color is empty (has no effect on a VisualLineTextElement). |
---|
227 | /// For example, the C# "Punctuation" is an empty color. |
---|
228 | /// </summary> |
---|
229 | internal static bool IsEmptyColor(HighlightingColor color) |
---|
230 | { |
---|
231 | if (color == null) |
---|
232 | return true; |
---|
233 | return color.Background == null && color.Foreground == null |
---|
234 | && color.FontStyle == null && color.FontWeight == null; |
---|
235 | } |
---|
236 | |
---|
237 | /// <summary> |
---|
238 | /// Applies a highlighting color to a visual line element. |
---|
239 | /// </summary> |
---|
240 | protected virtual void ApplyColorToElement(VisualLineElement element, HighlightingColor color) |
---|
241 | { |
---|
242 | ApplyColorToElement(element, color, CurrentContext); |
---|
243 | } |
---|
244 | |
---|
245 | internal static void ApplyColorToElement(VisualLineElement element, HighlightingColor color, ITextRunConstructionContext context) |
---|
246 | { |
---|
247 | if (color.Foreground != null) { |
---|
248 | Brush b = color.Foreground.GetBrush(context); |
---|
249 | if (b != null) |
---|
250 | element.TextRunProperties.SetForegroundBrush(b); |
---|
251 | } |
---|
252 | if (color.Background != null) { |
---|
253 | Brush b = color.Background.GetBrush(context); |
---|
254 | if (b != null) |
---|
255 | element.BackgroundBrush = b; |
---|
256 | } |
---|
257 | if (color.FontStyle != null || color.FontWeight != null) { |
---|
258 | Typeface tf = element.TextRunProperties.Typeface; |
---|
259 | element.TextRunProperties.SetTypeface(new Typeface( |
---|
260 | tf.FontFamily, |
---|
261 | color.FontStyle ?? tf.Style, |
---|
262 | color.FontWeight ?? tf.Weight, |
---|
263 | tf.Stretch |
---|
264 | )); |
---|
265 | } |
---|
266 | } |
---|
267 | |
---|
268 | /// <summary> |
---|
269 | /// This method is responsible for telling the TextView to redraw lines when the highlighting state has changed. |
---|
270 | /// </summary> |
---|
271 | /// <remarks> |
---|
272 | /// Creation of a VisualLine triggers the syntax highlighter (which works on-demand), so it says: |
---|
273 | /// Hey, the user typed "/*". Don't just recreate that line, but also the next one |
---|
274 | /// because my highlighting state (at end of line) changed! |
---|
275 | /// </remarks> |
---|
276 | void OnHighlightStateChanged(int fromLineNumber, int toLineNumber) |
---|
277 | { |
---|
278 | if (lineNumberBeingColorized != 0) { |
---|
279 | // Ignore notifications for any line except the one we're interested in. |
---|
280 | // This improves the performance as Redraw() can take quite some time when called repeatedly |
---|
281 | // while scanning the document (above the visible area) for highlighting changes. |
---|
282 | if (toLineNumber <= lineNumberBeingColorized) { |
---|
283 | return; |
---|
284 | } |
---|
285 | } |
---|
286 | |
---|
287 | // The user may have inserted "/*" into the current line, and so far only that line got redrawn. |
---|
288 | // So when the highlighting state is changed, we issue a redraw for the line immediately below. |
---|
289 | // If the highlighting state change applies to the lines below, too, the construction of each line |
---|
290 | // will invalidate the next line, and the construction pass will regenerate all lines. |
---|
291 | |
---|
292 | Debug.WriteLine(string.Format("OnHighlightStateChanged forces redraw of lines {0} to {1}", fromLineNumber, toLineNumber)); |
---|
293 | |
---|
294 | // If the VisualLine construction is in progress, we have to avoid sending redraw commands for |
---|
295 | // anything above the line currently being constructed. |
---|
296 | // It takes some explanation to see why this cannot happen. |
---|
297 | // VisualLines always get constructed from top to bottom. |
---|
298 | // Each VisualLine construction calls into the highlighter and thus forces an update of the |
---|
299 | // highlighting state for all lines up to the one being constructed. |
---|
300 | |
---|
301 | // To guarantee that we don't redraw lines we just constructed, we need to show that when |
---|
302 | // a VisualLine is being reused, the highlighting state at that location is still up-to-date. |
---|
303 | |
---|
304 | // This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes |
---|
305 | // (e.g. split view). |
---|
306 | |
---|
307 | // For the first line in the view, the TextView.VisualLineConstructionStarting event is used to check that the |
---|
308 | // highlighting state is up-to-date. If it isn't, this method will be executed, and it'll mark the first line |
---|
309 | // in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused. |
---|
310 | |
---|
311 | // Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom |
---|
312 | // construction process. |
---|
313 | |
---|
314 | // We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1. |
---|
315 | |
---|
316 | // Start of induction: the first line in view is reused only if the highlighting state was up-to-date |
---|
317 | // until line N-1 (no change detected in VisualLineConstructionStarting event). |
---|
318 | |
---|
319 | // Induction step: |
---|
320 | // If another line N+1 is being reused, then either |
---|
321 | // a) the previous line (the visual line containing document line N) was newly constructed |
---|
322 | // or b) the previous line was reused |
---|
323 | // In case a, the construction updated the highlighting state. This means the stack at end of line N is up-to-date. |
---|
324 | // In case b, the highlighting state at N-1 was up-to-date, and the text of line N was not changed. |
---|
325 | // (if the text was changed, the line could not have been reused). |
---|
326 | // From this follows that the highlighting state at N is still up-to-date. |
---|
327 | |
---|
328 | // The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line. |
---|
329 | // Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine, |
---|
330 | // so it will always invalidate the next visual line when a folded line is constructed |
---|
331 | // and the highlighting stack has changed. |
---|
332 | |
---|
333 | if (fromLineNumber == toLineNumber) { |
---|
334 | textView.Redraw(textView.Document.GetLineByNumber(fromLineNumber)); |
---|
335 | } else { |
---|
336 | // If there are multiple lines marked as changed; only the first one really matters |
---|
337 | // for the highlighting during rendering. |
---|
338 | // However this callback is also called outside of the rendering process, e.g. when a highlighter |
---|
339 | // decides to re-highlight some section based on external feedback (e.g. semantic highlighting). |
---|
340 | var fromLine = textView.Document.GetLineByNumber(fromLineNumber); |
---|
341 | var toLine = textView.Document.GetLineByNumber(toLineNumber); |
---|
342 | int startOffset = fromLine.Offset; |
---|
343 | textView.Redraw(startOffset, toLine.EndOffset - startOffset); |
---|
344 | } |
---|
345 | |
---|
346 | /* |
---|
347 | * Meta-comment: "why does this have to be so complicated?" |
---|
348 | * |
---|
349 | * The problem is that I want to re-highlight only on-demand and incrementally; |
---|
350 | * and at the same time only repaint changed lines. |
---|
351 | * So the highlighter and the VisualLine construction both have to run in a single pass. |
---|
352 | * The highlighter must take care that it never touches already constructed visual lines; |
---|
353 | * if it detects that something must be redrawn because the highlighting state changed, |
---|
354 | * it must do so early enough in the construction process. |
---|
355 | * But doing it too early means it doesn't have the information necessary to re-highlight and redraw only the desired parts. |
---|
356 | */ |
---|
357 | } |
---|
358 | } |
---|
359 | } |
---|