Free cookie consent management tool by TermsFeed Policy Generator

source: branches/PersistenceOverhaul/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Highlighting/HighlightingColorizer.cs

Last change on this file was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 15.3 KB
Line 
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
19using System;
20using System.Diagnostics;
21using System.Windows.Media;
22using ICSharpCode.AvalonEdit.Document;
23using ICSharpCode.AvalonEdit.Rendering;
24
25namespace 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}
Note: See TracBrowser for help on using the repository browser.