@ -18,6 +18,8 @@ namespace ICSharpCode.AvalonEdit.Highlighting
public class HighlightingColorizer : DocumentColorizingTransformer
public class HighlightingColorizer : DocumentColorizingTransformer
{
{
readonly HighlightingRuleSet ruleSet ;
readonly HighlightingRuleSet ruleSet ;
TextView textView ;
IHighlighter highlighter ;
/// <summary>
/// <summary>
/// Creates a new HighlightingColorizer instance.
/// Creates a new HighlightingColorizer instance.
@ -43,8 +45,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary>
/// </summary>
protected virtual void DeregisterServices ( TextView textView )
protected virtual void DeregisterServices ( TextView textView )
{
{
// remove existing highlighter, if any exists
if ( highlighter ! = null ) {
textView . Services . RemoveService ( typeof ( IHighlighter ) ) ;
highlighter . HighlightingStateChanged - = OnHighlightStateChanged ;
// remove highlighter if it is registered
if ( textView . Services . GetService ( typeof ( IHighlighter ) ) = = highlighter )
textView . Services . RemoveService ( typeof ( IHighlighter ) ) ;
}
}
}
/// <summary>
/// <summary>
@ -53,10 +60,15 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary>
/// </summary>
protected virtual void RegisterServices ( TextView textView )
protected virtual void RegisterServices ( TextView textView )
{
{
TextDocument document = textView . Document ;
if ( textView . Document ! = null ) {
if ( document ! = null ) {
highlighter = textView . Document ! = null ? CreateHighlighter ( textView , textView . Document ) : null ;
IHighlighter highlighter = CreateHighlighter ( textView , document ) ;
if ( highlighter ! = null ) {
textView . Services . AddService ( typeof ( IHighlighter ) , highlighter ) ;
// add service only if it doesn't already exist
if ( textView . Services . GetService ( typeof ( IHighlighter ) ) = = null ) {
textView . Services . AddService ( typeof ( IHighlighter ) , highlighter ) ;
}
highlighter . HighlightingStateChanged + = OnHighlightStateChanged ;
}
}
}
}
}
@ -65,13 +77,17 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// </summary>
/// </summary>
protected virtual IHighlighter CreateHighlighter ( TextView textView , TextDocument document )
protected virtual IHighlighter CreateHighlighter ( TextView textView , TextDocument document )
{
{
return new TextView DocumentHighlighter( this , textView , document , ruleSet ) ;
return new DocumentHighlighter ( document , ruleSet ) ;
}
}
/// <inheritdoc/>
/// <inheritdoc/>
protected override void OnAddToTextView ( TextView textView )
protected override void OnAddToTextView ( TextView textView )
{
{
if ( this . textView ! = null ) {
throw new InvalidOperationException ( "Cannot use a HighlightingColorizer instance in multiple text views. Please create a separate instance for each text view." ) ;
}
base . OnAddToTextView ( textView ) ;
base . OnAddToTextView ( textView ) ;
this . textView = textView ;
textView . DocumentChanged + = textView_DocumentChanged ;
textView . DocumentChanged + = textView_DocumentChanged ;
textView . VisualLineConstructionStarting + = textView_VisualLineConstructionStarting ;
textView . VisualLineConstructionStarting + = textView_VisualLineConstructionStarting ;
RegisterServices ( textView ) ;
RegisterServices ( textView ) ;
@ -84,18 +100,18 @@ namespace ICSharpCode.AvalonEdit.Highlighting
textView . DocumentChanged - = textView_DocumentChanged ;
textView . DocumentChanged - = textView_DocumentChanged ;
textView . VisualLineConstructionStarting - = textView_VisualLineConstructionStarting ;
textView . VisualLineConstructionStarting - = textView_VisualLineConstructionStarting ;
base . OnRemoveFromTextView ( textView ) ;
base . OnRemoveFromTextView ( textView ) ;
this . textView = null ;
}
}
void textView_VisualLineConstructionStarting ( object sender , VisualLineConstructionStartEventArgs e )
void textView_VisualLineConstructionStarting ( object sender , VisualLineConstructionStartEventArgs e )
{
{
IHighlighter highlighter = ( ( TextView ) sender ) . Services . GetService ( typeof ( IHighlighter ) ) as IHighlighter ;
if ( highlighter ! = null ) {
if ( highlighter ! = null ) {
// Force update of highlighting state up to the position where we start generating visual lines.
// Force update of highlighting state up to the position where we start generating visual lines.
// This is necessary in case the document gets modified above the FirstLineInView so that the highlighting state changes.
// This is necessary in case the document gets modified above the FirstLineInView so that the highlighting state changes.
// We need to detect this case and issue a redraw (through TextViewDocumentHighligher.OnHighlightStateChanged)
// We need to detect this case and issue a redraw (through TextViewDocumentHighligher.OnHighlightStateChanged)
// before the visual line construction reuses existing lines that were built using the invalid highlighting state.
// before the visual line construction reuses existing lines that were built using the invalid highlighting state.
lineNumberBeingColorized = e . FirstLineInView . LineNumber - 1 ;
lineNumberBeingColorized = e . FirstLineInView . LineNumber - 1 ;
highlighter . GetColorStack ( lineNumberBeingColorized ) ;
highlighter . UpdateHighlightingState ( lineNumberBeingColorized ) ;
lineNumberBeingColorized = 0 ;
lineNumberBeingColorized = 0 ;
}
}
}
}
@ -108,14 +124,13 @@ namespace ICSharpCode.AvalonEdit.Highlighting
this . lastColorizedLine = null ;
this . lastColorizedLine = null ;
base . Colorize ( context ) ;
base . Colorize ( context ) ;
if ( this . lastColorizedLine ! = context . VisualLine . LastDocumentLine ) {
if ( this . lastColorizedLine ! = context . VisualLine . LastDocumentLine ) {
IHighlighter highlighter = context . TextView . Services . GetService ( typeof ( IHighlighter ) ) as IHighlighter ;
if ( highlighter ! = null ) {
if ( highlighter ! = null ) {
// In some cases, it is possible that we didn't highlight the last document line within the visual line
// In some cases, it is possible that we didn't highlight the last document line within the visual line
// (e.g. when the line ends with a fold marker).
// (e.g. when the line ends with a fold marker).
// But even if we didn't highlight it, we'll have to update the highlighting state for it so that the
// But even if we didn't highlight it, we'll have to update the highlighting state for it so that the
// proof inside TextViewDocumentHighlighter.OnHighlightStateChanged holds.
// proof inside TextViewDocumentHighlighter.OnHighlightStateChanged holds.
lineNumberBeingColorized = context . VisualLine . LastDocumentLine . LineNumber ;
lineNumberBeingColorized = context . VisualLine . LastDocumentLine . LineNumber ;
highlighter . GetColorStack ( lineNumberBeingColorized ) ;
highlighter . UpdateHighlightingState ( lineNumberBeingColorized ) ;
lineNumberBeingColorized = 0 ;
lineNumberBeingColorized = 0 ;
}
}
}
}
@ -127,7 +142,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting
/// <inheritdoc/>
/// <inheritdoc/>
protected override void ColorizeLine ( DocumentLine line )
protected override void ColorizeLine ( DocumentLine line )
{
{
IHighlighter highlighter = CurrentContext . TextView . Services . GetService ( typeof ( IHighlighter ) ) as IHighlighter ;
if ( highlighter ! = null ) {
if ( highlighter ! = null ) {
lineNumberBeingColorized = line . LineNumber ;
lineNumberBeingColorized = line . LineNumber ;
HighlightedLine hl = highlighter . HighlightLine ( lineNumberBeingColorized ) ;
HighlightedLine hl = highlighter . HighlightLine ( lineNumberBeingColorized ) ;
@ -181,102 +195,82 @@ namespace ICSharpCode.AvalonEdit.Highlighting
}
}
/// <summary>
/// <summary>
/// This class is responsible for telling the TextView to redraw lines when the highlighting state has changed.
/// This method is responsible for telling the TextView to redraw lines when the highlighting state has changed.
/// </summary>
/// </summary>
/// <remarks>
/// <remarks>
/// Creation of a VisualLine triggers the syntax highlighter (which works on-demand), so it says:
/// Creation of a VisualLine triggers the syntax highlighter (which works on-demand), so it says:
/// Hey, the user typed "/*". Don't just recreate that line, but also the next one
/// Hey, the user typed "/*". Don't just recreate that line, but also the next one
/// because my highlighting state (at end of line) changed!
/// because my highlighting state (at end of line) changed!
/// </remarks>
/// </remarks>
sealed class TextViewDocumentHighlighter : DocumentHighlighter
void OnHighlightStateChanged ( IHighlighter sender , int lineNumber )
{
{
readonly HighlightingColorizer colorizer ;
if ( lineNumberBeingColorized ! = lineNumber ) {
readonly TextView textView ;
// Ignore notifications for any line except the one we're interested in.
// This improves the performance as Redraw() can take quite some time when called repeatedly
public TextViewDocumentHighlighter ( HighlightingColorizer colorizer , TextView textView , TextDocument document , HighlightingRuleSet baseRuleSet )
// while scanning the document (above the visible area) for highlighting changes.
: base ( document , baseRuleSet )
return ;
{
Debug . Assert ( colorizer ! = null ) ;
Debug . Assert ( textView ! = null ) ;
this . colorizer = colorizer ;
this . textView = textView ;
}
}
protected override void OnHighlightStateChanged ( IDocumentLine line , int lineNumber )
// The user may have inserted "/*" into the current line, and so far only that line got redrawn.
{
// So when the highlighting state is changed, we issue a redraw for the line immediately below.
base . OnHighlightStateChanged ( line , lineNumber ) ;
// If the highlighting state change applies to the lines below, too, the construction of each line
if ( colorizer . lineNumberBeingColorized ! = lineNumber ) {
// will invalidate the next line, and the construction pass will regenerate all lines.
// Ignore notifications for any line except the one we're interested in.
// This improves the performance as Redraw() can take quite some time when called repeatedly
Debug . WriteLine ( "OnHighlightStateChanged forces redraw of line " + ( lineNumber + 1 ) ) ;
// while scanning the document (above the visible area) for highlighting changes.
return ;
// If the VisualLine construction is in progress, we have to avoid sending redraw commands for
}
// anything above the line currently being constructed.
if ( textView . Document ! = this . Document ) {
// It takes some explanation to see why this cannot happen.
// May happen if document on text view was changed but some user code is still using the
// VisualLines always get constructed from top to bottom.
// existing IHighlighter instance.
// Each VisualLine construction calls into the highlighter and thus forces an update of the
return ;
// highlighting state for all lines up to the one being constructed.
}
// To guarantee that we don't redraw lines we just constructed, we need to show that when
// The user may have inserted "/*" into the current line, and so far only that line got redrawn.
// a VisualLine is being reused, the highlighting state at that location is still up-to-date.
// So when the highlighting state is changed, we issue a redraw for the line immediately below.
// If the highlighting state change applies to the lines below, too, the construction of each line
// This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes
// will invalidate the next line, and the construction pass will regenerate all lines.
// (e.g. split view).
Debug . WriteLine ( "OnHighlightStateChanged forces redraw of line " + ( lineNumber + 1 ) ) ;
// For the first line in the view, the TextView.VisualLineConstructionStarting event is used to check that the
// highlighting state is up-to-date. If it isn't, this method will be executed, and it'll mark the first line
// If the VisualLine construction is in progress, we have to avoid sending redraw commands for
// in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused.
// anything above the line currently being constructed.
// It takes some explanation to see why this cannot happen.
// Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom
// VisualLines always get constructed from top to bottom.
// construction process.
// Each VisualLine construction calls into the highlighter and thus forces an update of the
// highlighting state for all lines up to the one being constructed.
// We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1.
// To guarantee that we don't redraw lines we just constructed, we need to show that when
// Start of induction: the first line in view is reused only if the highlighting state was up-to-date
// a VisualLine is being reused, the highlighting state at that location is still up-to-date.
// until line N-1 (no change detected in VisualLineConstructionStarting event).
// This isn't exactly trivial and the initial implementation was incorrect in the presence of external document changes
// Induction step:
// (e.g. split view).
// If another line N+1 is being reused, then either
// a) the previous line (the visual line containing document line N) was newly constructed
// For the first line in the view, the TextView.VisualLineConstructionStarting event is used to check that the
// or b) the previous line was reused
// highlighting state is up-to-date. If it isn't, this method will be executed, and it'll mark the first line
// In case a, the construction updated the highlighting state. This means the stack at end of line N is up-to-date.
// in the view as requiring a redraw. This is safely possible because that event occurs before any lines are reused.
// In case b, the highlighting state at N-1 was up-to-date, and the text of line N was not changed.
// (if the text was changed, the line could not have been reused).
// Once we take care of the first visual line, we won't get in trouble with other lines due to the top-to-bottom
// From this follows that the highlighting state at N is still up-to-date.
// construction process.
// The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line.
// We'll prove that: if line N is being reused, then the highlighting state is up-to-date until (end of) line N-1.
// Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine,
// so it will always invalidate the next visual line when a folded line is constructed
// Start of induction: the first line in view is reused only if the highlighting state was up-to-date
// and the highlighting stack has changed.
// until line N-1 (no change detected in VisualLineConstructionStarting event).
if ( lineNumber + 1 < = textView . Document . LineCount )
// Induction step:
textView . Redraw ( textView . Document . GetLineByNumber ( lineNumber + 1 ) , DispatcherPriority . Normal ) ;
// If another line N+1 is being reused, then either
// a) the previous line (the visual line containing document line N) was newly constructed
/ *
// or b) the previous line was reused
* Meta - comment : "why does this have to be so complicated?"
// In case a, the construction updated the highlighting state. This means the stack at end of line N is up-to-date.
*
// In case b, the highlighting state at N-1 was up-to-date, and the text of line N was not changed.
* The problem is that I want to re - highlight only on - demand and incrementally ;
// (if the text was changed, the line could not have been reused).
* and at the same time only repaint changed lines .
// From this follows that the highlighting state at N is still up-to-date.
* So the highlighter and the VisualLine construction both have to run in a single pass .
* The highlighter must take care that it never touches already constructed visual lines ;
// The above proof holds even in the presence of folding: folding only ever hides text in the middle of a visual line.
* if it detects that something must be redrawn because the highlighting state changed ,
// Our Colorize-override ensures that the highlighting state is always updated for the LastDocumentLine,
* it must do so early enough in the construction process .
// so it will always invalidate the next visual line when a folded line is constructed
* But doing it too early means it doesn ' t have the information necessary to re - highlight and redraw only the desired parts .
// and the highlighting stack has changed.
* /
textView . Redraw ( line . NextLine , DispatcherPriority . Normal ) ;
/ *
* Meta - comment : "why does this have to be so complicated?"
*
* The problem is that I want to re - highlight only on - demand and incrementally ;
* and at the same time only repaint changed lines .
* So the highlighter and the VisualLine construction both have to run in a single pass .
* The highlighter must take care that it never touches already constructed visual lines ;
* if it detects that something must be redrawn because the highlighting state changed ,
* it must do so early enough in the construction process .
* But doing it too early means it doesn ' t have the information necessary to re - highlight and redraw only the desired parts .
* /
}
}
}
}
}
}
}