Skip to content
Browse files

Suppress flickering in semantic highlighting.

We now cache the semantic highlighting for the visible lines, and re-use the existing highlighting sections when no new parse information is available.
  • Loading branch information...
1 parent c001357 commit 889361a446df8911cd04124fc4cbb1eea97d5c48 @dgrunwald dgrunwald committed
View
117 src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs
@@ -29,7 +29,65 @@ public class CSharpSemanticHighlighter : DepthFirstAstVisitor<object, object>, I
readonly HighlightingColor fieldAccessColor;
readonly HighlightingColor valueKeywordColor;
- HashSet<IDocumentLine> invalidLines = new HashSet<IDocumentLine>();
+ List<IDocumentLine> invalidLines = new List<IDocumentLine>();
+ List<CachedLine> cachedLines = new List<CachedLine>();
+
+ // If a line gets edited and we need to display it while no parse information is ready for the
+ // changed file, the line would flicker (semantic highlightings disappear temporarily).
+ // We avoid this issue by storing the semantic highlightings and updating them on document changes
+ // (using anchor movement)
+ class CachedLine
+ {
+ public readonly HighlightedLine HighlightedLine;
+ public ITextSourceVersion OldVersion;
+
+ /// <summary>
+ /// Gets whether the cache line is valid (no document changes since it was created).
+ /// This field gets set to false when Update() is called.
+ /// </summary>
+ public bool IsValid;
+
+ public IDocumentLine DocumentLine { get { return HighlightedLine.DocumentLine; } }
+
+ public CachedLine(HighlightedLine highlightedLine, ITextSourceVersion fileVersion)
+ {
+ if (highlightedLine == null)
+ throw new ArgumentNullException("highlightedLine");
+ if (fileVersion == null)
+ throw new ArgumentNullException("fileVersion");
+
+ this.HighlightedLine = highlightedLine;
+ this.OldVersion = fileVersion;
+ this.IsValid = true;
+ }
+
+ public void Update(ITextSourceVersion newVersion)
+ {
+ // Apply document changes to all highlighting sections:
+ foreach (TextChangeEventArgs change in OldVersion.GetChangesTo(newVersion)) {
+ foreach (HighlightedSection section in HighlightedLine.Sections) {
+ int endOffset = section.Offset + section.Length;
+ section.Offset = change.GetNewOffset(section.Offset);
+ endOffset = change.GetNewOffset(endOffset);
+ section.Length = endOffset - section.Offset;
+ }
+ }
+ // The resulting sections might have become invalid:
+ // - zero-length if section was deleted,
+ // - a section might have moved outside the range of this document line (newline inserted in document = line split up)
+ // So we will remove all highlighting sections which have become invalid.
+ int lineStart = HighlightedLine.DocumentLine.Offset;
+ int lineEnd = lineStart + HighlightedLine.DocumentLine.Length;
+ for (int i = 0; i < HighlightedLine.Sections.Count; i++) {
+ HighlightedSection section = HighlightedLine.Sections[i];
+ if (section.Offset < lineStart || section.Offset + section.Length > lineEnd || section.Length <= 0)
+ HighlightedLine.Sections.RemoveAt(i--);
+ }
+
+ this.OldVersion = newVersion;
+ this.IsValid = false;
+ }
+ }
int lineNumber;
HighlightedLine line;
@@ -52,24 +110,36 @@ public CSharpSemanticHighlighter(ITextEditor textEditor, ISyntaxHighlighter synt
this.fieldAccessColor = highlightingDefinition.GetNamedColor("FieldAccess");
this.valueKeywordColor = highlightingDefinition.GetNamedColor("NullOrValueKeywords");
- ParserService.ParserUpdateStepFinished += ParserService_ParserUpdateStepFinished;
+ ParserService.ParseInformationUpdated += ParserService_ParseInformationUpdated;
ParserService.LoadSolutionProjectsThreadEnded += ParserService_LoadSolutionProjectsThreadEnded;
+ syntaxHighlighter.VisibleDocumentLinesChanged += syntaxHighlighter_VisibleDocumentLinesChanged;
}
public void Dispose()
{
- ParserService.ParserUpdateStepFinished -= ParserService_ParserUpdateStepFinished;
+ ParserService.ParseInformationUpdated -= ParserService_ParseInformationUpdated;
ParserService.LoadSolutionProjectsThreadEnded -= ParserService_LoadSolutionProjectsThreadEnded;
+ syntaxHighlighter.VisibleDocumentLinesChanged -= syntaxHighlighter_VisibleDocumentLinesChanged;
+ }
+
+ void syntaxHighlighter_VisibleDocumentLinesChanged(object sender, EventArgs e)
+ {
+ // use this event to remove cached lines which are no longer visible
+ var visibleDocumentLines = new HashSet<IDocumentLine>(syntaxHighlighter.GetVisibleDocumentLines());
+ cachedLines.RemoveAll(c => !visibleDocumentLines.Contains(c.DocumentLine));
}
void ParserService_LoadSolutionProjectsThreadEnded(object sender, EventArgs e)
{
+ cachedLines.Clear();
+ invalidLines.Clear();
syntaxHighlighter.InvalidateAll();
}
- void ParserService_ParserUpdateStepFinished(object sender, ParserUpdateStepEventArgs e)
+ void ParserService_ParseInformationUpdated(object sender, ParseInformationEventArgs e)
{
if (e.FileName == textEditor.FileName && invalidLines.Count > 0) {
+ cachedLines.Clear();
foreach (IDocumentLine line in invalidLines) {
if (!line.IsDeleted) {
syntaxHighlighter.InvalidateLine(line);
@@ -90,12 +160,42 @@ IEnumerable<HighlightingColor> IHighlighter.GetColorStack(int lineNumber)
public HighlightedLine HighlightLine(int lineNumber)
{
+ IDocumentLine documentLine = textEditor.Document.GetLineByNumber(lineNumber);
+ ITextSourceVersion newVersion = textEditor.Document.Version;
+ CachedLine cachedLine = null;
+ for (int i = 0; i < cachedLines.Count; i++) {
+ if (cachedLines[i].DocumentLine == documentLine) {
+ if (newVersion == null || !newVersion.BelongsToSameDocumentAs(cachedLines[i].OldVersion)) {
+ // cannot list changes from old to new: we can't update the cache, so we'll remove it
+ cachedLines.RemoveAt(i);
+ } else {
+ cachedLine = cachedLines[i];
+ }
+ break;
+ }
+ }
+
+ if (cachedLine != null && cachedLine.IsValid && newVersion.CompareAge(cachedLine.OldVersion) == 0) {
+ // the file hasn't changed since the cache was created, so just reuse the old highlighted line
+ return cachedLine.HighlightedLine;
+ }
+
ParseInformation parseInfo = ParserService.GetCachedParseInformation(textEditor.FileName, textEditor.Document.Version);
if (parseInfo == null) {
- invalidLines.Add(textEditor.Document.GetLineByNumber(lineNumber));
+ if (!invalidLines.Contains(documentLine))
+ invalidLines.Add(documentLine);
Debug.WriteLine("Semantic highlighting for line {0} - marking as invalid", lineNumber);
- return null;
+
+ if (cachedLine != null) {
+ // If there's a cached version, adjust it to the latest document changes and return it.
+ // This avoids flickering when changing a line that contains semantic highlighting.
+ cachedLine.Update(newVersion);
+ return cachedLine.HighlightedLine;
+ } else {
+ return null;
+ }
}
+
CSharpParsedFile parsedFile = parseInfo.ParsedFile as CSharpParsedFile;
CompilationUnit cu = parseInfo.Annotation<CompilationUnit>();
if (cu == null || parsedFile == null) {
@@ -109,13 +209,16 @@ public HighlightedLine HighlightLine(int lineNumber)
resolveVisitor.Scan(cu);
- HighlightedLine line = new HighlightedLine(textEditor.Document, textEditor.Document.GetLineByNumber(lineNumber));
+ HighlightedLine line = new HighlightedLine(textEditor.Document, documentLine);
this.line = line;
this.lineNumber = lineNumber;
cu.AcceptVisitor(this);
this.line = null;
this.resolveVisitor = null;
Debug.WriteLine("Semantic highlighting for line {0} - added {1} sections", lineNumber, line.Sections.Count);
+ if (textEditor.Document.Version != null) {
+ cachedLines.Add(new CachedLine(line, textEditor.Document.Version));
+ }
return line;
}
}
View
27 src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs
@@ -343,6 +343,33 @@ public void InvalidateAll()
{
textView.Redraw(DispatcherPriority.Background);
}
+
+ public event EventHandler VisibleDocumentLinesChanged {
+ add { textView.VisualLinesChanged += value; }
+ remove { textView.VisualLinesChanged -= value; }
+ }
+
+ public IEnumerable<IDocumentLine> GetVisibleDocumentLines()
+ {
+ List<IDocumentLine> result = new List<IDocumentLine>();
+ foreach (VisualLine line in textView.VisualLines) {
+ if (line.FirstDocumentLine == line.LastDocumentLine) {
+ result.Add(line.FirstDocumentLine);
+ } else {
+ int firstLineStart = line.FirstDocumentLine.Offset;
+ int lineEndOffset = firstLineStart + line.FirstDocumentLine.TotalLength;
+ foreach (VisualLineElement e in line.Elements) {
+ int elementOffset = firstLineStart + e.RelativeTextOffset;
+ if (elementOffset >= lineEndOffset) {
+ var currentLine = this.Document.GetLineByOffset(elementOffset);
+ lineEndOffset = currentLine.Offset + currentLine.TotalLength;
+ result.Add(currentLine);
+ }
+ }
+ }
+ }
+ return result;
+ }
}
sealed class CustomizedBrush : HighlightingBrush
View
4 src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs
@@ -10,7 +10,7 @@
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
-using ICSharpCode.SharpDevelop.Editor;
+using ICSharpCode.NRefactory.Editor;
using ICSharpCode.SharpDevelop.Editor.CodeCompletion;
namespace ICSharpCode.AvalonEdit.AddIn
@@ -148,7 +148,7 @@ protected override void DetachEvents()
void document_Changed(object sender, DocumentChangeEventArgs e)
{
if (DocumentChanged != null)
- DocumentChanged(this, new TextChangeEventArgs(e.Offset, e.RemovedText, e.InsertedText));
+ DocumentChanged(this, e);
}
public event EventHandler<TextChangeEventArgs> DocumentChanged;
View
2 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
@@ -49,7 +49,7 @@ internal OffsetChangeMapEntry CreateSingleChangeMapEntry()
/// <summary>
/// Gets the new offset where the specified offset moves after this document change.
/// </summary>
- public int GetNewOffset(int offset, AnchorMovementType movementType)
+ public override int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default)
{
if (offsetChangeMap != null)
return offsetChangeMap.GetNewOffset(offset, movementType);
View
4 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs
@@ -33,6 +33,7 @@ protected override void Colorize(ITextRunConstructionContext context)
currentDocumentLine = context.VisualLine.FirstDocumentLine;
firstLineStart = currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
+ int currentDocumentLineTotalEndOffset = currentDocumentLineStartOffset + currentDocumentLine.TotalLength;
if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) {
ColorizeLine(currentDocumentLine);
@@ -41,10 +42,11 @@ protected override void Colorize(ITextRunConstructionContext context)
// ColorizeLine modifies the visual line elements, loop through a copy of the line elements
foreach (VisualLineElement e in context.VisualLine.Elements.ToArray()) {
int elementOffset = firstLineStart + e.RelativeTextOffset;
- if (elementOffset >= currentDocumentLineEndOffset) {
+ if (elementOffset >= currentDocumentLineTotalEndOffset) {
currentDocumentLine = context.Document.GetLineByOffset(elementOffset);
currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
+ currentDocumentLineTotalEndOffset = currentDocumentLineStartOffset + currentDocumentLine.TotalLength;
ColorizeLine(currentDocumentLine);
}
}
View
1 src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj
@@ -167,7 +167,6 @@
<Compile Include="Src\Editor\Search\SearchResultMatch.cs" />
<Compile Include="Src\Editor\Search\SearchResultsPad.cs" />
<Compile Include="Src\Editor\Search\SearchResultPadToolbarCommands.cs" />
- <Compile Include="Src\Editor\TextChangeEventArgs.cs" />
<Compile Include="Src\Editor\TextNavigationPoint.cs" />
<Compile Include="Src\Editor\ToolTipRequestEventArgs.cs">
<DependentUpon>ToolTipService.cs</DependentUpon>
View
1 src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using ICSharpCode.NRefactory.Editor;
namespace ICSharpCode.SharpDevelop.Editor.CodeCompletion
{
View
10 src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs
@@ -52,6 +52,16 @@ public interface ISyntaxHighlighter
/// (e.g. semantic highlighting).
/// </remarks>
void InvalidateAll();
+
+ /// <summary>
+ /// Gets the document lines that are currently visible in the editor.
+ /// </summary>
+ IEnumerable<IDocumentLine> GetVisibleDocumentLines();
+
+ /// <summary>
+ /// Raised when the set of visible document lines has changed.
+ /// </summary>
+ event EventHandler VisibleDocumentLinesChanged;
}
public static class SyntaxHighligherKnownSpanNames
View
53 src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs
@@ -1,53 +0,0 @@
-// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
-// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
-
-using System;
-
-namespace ICSharpCode.SharpDevelop.Editor
-{
- /// <summary>
- /// Describes a change of the document text.
- /// This class is thread-safe.
- /// </summary>
- public class TextChangeEventArgs : EventArgs
- {
- /// <summary>
- /// The offset at which the change occurs.
- /// </summary>
- public int Offset { get; private set; }
-
- /// <summary>
- /// The text that was inserted.
- /// </summary>
- public string RemovedText { get; private set; }
-
- /// <summary>
- /// The number of characters removed.
- /// </summary>
- public int RemovalLength {
- get { return RemovedText.Length; }
- }
-
- /// <summary>
- /// The text that was inserted.
- /// </summary>
- public string InsertedText { get; private set; }
-
- /// <summary>
- /// The number of characters inserted.
- /// </summary>
- public int InsertionLength {
- get { return InsertedText.Length; }
- }
-
- /// <summary>
- /// Creates a new TextChangeEventArgs object.
- /// </summary>
- public TextChangeEventArgs(int offset, string removedText, string insertedText)
- {
- this.Offset = offset;
- this.RemovedText = removedText ?? string.Empty;
- this.InsertedText = insertedText ?? string.Empty;
- }
- }
-}

0 comments on commit 889361a

Please sign in to comment.
Something went wrong with that request. Please try again.