Browse Source

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.
newNRvisualizers
Daniel Grunwald 14 years ago
parent
commit
889361a446
  1. 115
      src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs
  2. 27
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs
  3. 4
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs
  4. 2
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
  5. 4
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs
  6. 1
      src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj
  7. 1
      src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs
  8. 10
      src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs
  9. 53
      src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs

115
src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs

@ -29,7 +29,65 @@ namespace CSharpBinding
readonly HighlightingColor fieldAccessColor; readonly HighlightingColor fieldAccessColor;
readonly HighlightingColor valueKeywordColor; 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; int lineNumber;
HighlightedLine line; HighlightedLine line;
@ -52,24 +110,36 @@ namespace CSharpBinding
this.fieldAccessColor = highlightingDefinition.GetNamedColor("FieldAccess"); this.fieldAccessColor = highlightingDefinition.GetNamedColor("FieldAccess");
this.valueKeywordColor = highlightingDefinition.GetNamedColor("NullOrValueKeywords"); this.valueKeywordColor = highlightingDefinition.GetNamedColor("NullOrValueKeywords");
ParserService.ParserUpdateStepFinished += ParserService_ParserUpdateStepFinished; ParserService.ParseInformationUpdated += ParserService_ParseInformationUpdated;
ParserService.LoadSolutionProjectsThreadEnded += ParserService_LoadSolutionProjectsThreadEnded; ParserService.LoadSolutionProjectsThreadEnded += ParserService_LoadSolutionProjectsThreadEnded;
syntaxHighlighter.VisibleDocumentLinesChanged += syntaxHighlighter_VisibleDocumentLinesChanged;
} }
public void Dispose() public void Dispose()
{ {
ParserService.ParserUpdateStepFinished -= ParserService_ParserUpdateStepFinished; ParserService.ParseInformationUpdated -= ParserService_ParseInformationUpdated;
ParserService.LoadSolutionProjectsThreadEnded -= ParserService_LoadSolutionProjectsThreadEnded; 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) void ParserService_LoadSolutionProjectsThreadEnded(object sender, EventArgs e)
{ {
cachedLines.Clear();
invalidLines.Clear();
syntaxHighlighter.InvalidateAll(); syntaxHighlighter.InvalidateAll();
} }
void ParserService_ParserUpdateStepFinished(object sender, ParserUpdateStepEventArgs e) void ParserService_ParseInformationUpdated(object sender, ParseInformationEventArgs e)
{ {
if (e.FileName == textEditor.FileName && invalidLines.Count > 0) { if (e.FileName == textEditor.FileName && invalidLines.Count > 0) {
cachedLines.Clear();
foreach (IDocumentLine line in invalidLines) { foreach (IDocumentLine line in invalidLines) {
if (!line.IsDeleted) { if (!line.IsDeleted) {
syntaxHighlighter.InvalidateLine(line); syntaxHighlighter.InvalidateLine(line);
@ -90,12 +160,42 @@ namespace CSharpBinding
public HighlightedLine HighlightLine(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); ParseInformation parseInfo = ParserService.GetCachedParseInformation(textEditor.FileName, textEditor.Document.Version);
if (parseInfo == null) { 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); Debug.WriteLine("Semantic highlighting for line {0} - marking as invalid", lineNumber);
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; return null;
} }
}
CSharpParsedFile parsedFile = parseInfo.ParsedFile as CSharpParsedFile; CSharpParsedFile parsedFile = parseInfo.ParsedFile as CSharpParsedFile;
CompilationUnit cu = parseInfo.Annotation<CompilationUnit>(); CompilationUnit cu = parseInfo.Annotation<CompilationUnit>();
if (cu == null || parsedFile == null) { if (cu == null || parsedFile == null) {
@ -109,13 +209,16 @@ namespace CSharpBinding
resolveVisitor.Scan(cu); resolveVisitor.Scan(cu);
HighlightedLine line = new HighlightedLine(textEditor.Document, textEditor.Document.GetLineByNumber(lineNumber)); HighlightedLine line = new HighlightedLine(textEditor.Document, documentLine);
this.line = line; this.line = line;
this.lineNumber = lineNumber; this.lineNumber = lineNumber;
cu.AcceptVisitor(this); cu.AcceptVisitor(this);
this.line = null; this.line = null;
this.resolveVisitor = null; this.resolveVisitor = null;
Debug.WriteLine("Semantic highlighting for line {0} - added {1} sections", lineNumber, line.Sections.Count); 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; return line;
} }
} }

27
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs

@ -343,6 +343,33 @@ namespace ICSharpCode.AvalonEdit.AddIn
{ {
textView.Redraw(DispatcherPriority.Background); 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 sealed class CustomizedBrush : HighlightingBrush

4
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs

@ -10,7 +10,7 @@ using System.ComponentModel;
using ICSharpCode.AvalonEdit.CodeCompletion; using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing; using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.SharpDevelop.Editor; using ICSharpCode.NRefactory.Editor;
using ICSharpCode.SharpDevelop.Editor.CodeCompletion; using ICSharpCode.SharpDevelop.Editor.CodeCompletion;
namespace ICSharpCode.AvalonEdit.AddIn namespace ICSharpCode.AvalonEdit.AddIn
@ -148,7 +148,7 @@ namespace ICSharpCode.AvalonEdit.AddIn
void document_Changed(object sender, DocumentChangeEventArgs e) void document_Changed(object sender, DocumentChangeEventArgs e)
{ {
if (DocumentChanged != null) if (DocumentChanged != null)
DocumentChanged(this, new TextChangeEventArgs(e.Offset, e.RemovedText, e.InsertedText)); DocumentChanged(this, e);
} }
public event EventHandler<TextChangeEventArgs> DocumentChanged; public event EventHandler<TextChangeEventArgs> DocumentChanged;

2
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs

@ -49,7 +49,7 @@ namespace ICSharpCode.AvalonEdit.Document
/// <summary> /// <summary>
/// Gets the new offset where the specified offset moves after this document change. /// Gets the new offset where the specified offset moves after this document change.
/// </summary> /// </summary>
public int GetNewOffset(int offset, AnchorMovementType movementType) public override int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default)
{ {
if (offsetChangeMap != null) if (offsetChangeMap != null)
return offsetChangeMap.GetNewOffset(offset, movementType); return offsetChangeMap.GetNewOffset(offset, movementType);

4
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs

@ -33,6 +33,7 @@ namespace ICSharpCode.AvalonEdit.Rendering
currentDocumentLine = context.VisualLine.FirstDocumentLine; currentDocumentLine = context.VisualLine.FirstDocumentLine;
firstLineStart = currentDocumentLineStartOffset = currentDocumentLine.Offset; firstLineStart = currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length; currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
int currentDocumentLineTotalEndOffset = currentDocumentLineStartOffset + currentDocumentLine.TotalLength;
if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) { if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) {
ColorizeLine(currentDocumentLine); ColorizeLine(currentDocumentLine);
@ -41,10 +42,11 @@ namespace ICSharpCode.AvalonEdit.Rendering
// ColorizeLine modifies the visual line elements, loop through a copy of the line elements // ColorizeLine modifies the visual line elements, loop through a copy of the line elements
foreach (VisualLineElement e in context.VisualLine.Elements.ToArray()) { foreach (VisualLineElement e in context.VisualLine.Elements.ToArray()) {
int elementOffset = firstLineStart + e.RelativeTextOffset; int elementOffset = firstLineStart + e.RelativeTextOffset;
if (elementOffset >= currentDocumentLineEndOffset) { if (elementOffset >= currentDocumentLineTotalEndOffset) {
currentDocumentLine = context.Document.GetLineByOffset(elementOffset); currentDocumentLine = context.Document.GetLineByOffset(elementOffset);
currentDocumentLineStartOffset = currentDocumentLine.Offset; currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length; currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
currentDocumentLineTotalEndOffset = currentDocumentLineStartOffset + currentDocumentLine.TotalLength;
ColorizeLine(currentDocumentLine); ColorizeLine(currentDocumentLine);
} }
} }

1
src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj

@ -167,7 +167,6 @@
<Compile Include="Src\Editor\Search\SearchResultMatch.cs" /> <Compile Include="Src\Editor\Search\SearchResultMatch.cs" />
<Compile Include="Src\Editor\Search\SearchResultsPad.cs" /> <Compile Include="Src\Editor\Search\SearchResultsPad.cs" />
<Compile Include="Src\Editor\Search\SearchResultPadToolbarCommands.cs" /> <Compile Include="Src\Editor\Search\SearchResultPadToolbarCommands.cs" />
<Compile Include="Src\Editor\TextChangeEventArgs.cs" />
<Compile Include="Src\Editor\TextNavigationPoint.cs" /> <Compile Include="Src\Editor\TextNavigationPoint.cs" />
<Compile Include="Src\Editor\ToolTipRequestEventArgs.cs"> <Compile Include="Src\Editor\ToolTipRequestEventArgs.cs">
<DependentUpon>ToolTipService.cs</DependentUpon> <DependentUpon>ToolTipService.cs</DependentUpon>

1
src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using ICSharpCode.NRefactory.Editor;
namespace ICSharpCode.SharpDevelop.Editor.CodeCompletion namespace ICSharpCode.SharpDevelop.Editor.CodeCompletion
{ {

10
src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs

@ -52,6 +52,16 @@ namespace ICSharpCode.SharpDevelop.Editor
/// (e.g. semantic highlighting). /// (e.g. semantic highlighting).
/// </remarks> /// </remarks>
void InvalidateAll(); 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 public static class SyntaxHighligherKnownSpanNames

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;
}
}
}
Loading…
Cancel
Save