From 889361a446df8911cd04124fc4cbb1eea97d5c48 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 20 Sep 2011 02:50:21 +0200 Subject: [PATCH] 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. --- .../Project/Src/CSharpSemanticHighlighter.cs | 117 ++++++++++++++++-- .../Src/CustomizableHighlightingColorizer.cs | 27 ++++ .../Src/SharpDevelopInsightWindow.cs | 4 +- .../Document/DocumentChangeEventArgs.cs | 2 +- .../DocumentColorizingTransformer.cs | 4 +- .../Project/ICSharpCode.SharpDevelop.csproj | 1 - .../Editor/CodeCompletion/IInsightWindow.cs | 1 + .../Project/Src/Editor/ISyntaxHighlighter.cs | 10 ++ .../Project/Src/Editor/TextChangeEventArgs.cs | 53 -------- 9 files changed, 154 insertions(+), 65 deletions(-) delete mode 100644 src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs index 080676edbf..dcaec4b886 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs @@ -29,7 +29,65 @@ namespace CSharpBinding readonly HighlightingColor fieldAccessColor; readonly HighlightingColor valueKeywordColor; - HashSet invalidLines = new HashSet(); + List invalidLines = new List(); + List cachedLines = new List(); + + // 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; + + /// + /// Gets whether the cache line is valid (no document changes since it was created). + /// This field gets set to false when Update() is called. + /// + 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 @@ namespace CSharpBinding 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(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 @@ namespace CSharpBinding 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(); if (cu == null || parsedFile == null) { @@ -109,13 +209,16 @@ namespace CSharpBinding 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; } } diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs index 618890f04e..0c9e3952f4 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomizableHighlightingColorizer.cs @@ -343,6 +343,33 @@ namespace ICSharpCode.AvalonEdit.AddIn { textView.Redraw(DispatcherPriority.Background); } + + public event EventHandler VisibleDocumentLinesChanged { + add { textView.VisualLinesChanged += value; } + remove { textView.VisualLinesChanged -= value; } + } + + public IEnumerable GetVisibleDocumentLines() + { + List result = new List(); + 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 diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs index 49d014006c..18718abd6f 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopInsightWindow.cs @@ -10,7 +10,7 @@ using System.ComponentModel; 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 @@ namespace ICSharpCode.AvalonEdit.AddIn 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 DocumentChanged; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs index b563ae111a..901fef1606 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs @@ -49,7 +49,7 @@ namespace ICSharpCode.AvalonEdit.Document /// /// Gets the new offset where the specified offset moves after this document change. /// - 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); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs index 8237a44356..cf61ed9db3 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/DocumentColorizingTransformer.cs @@ -33,6 +33,7 @@ namespace ICSharpCode.AvalonEdit.Rendering 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 @@ namespace ICSharpCode.AvalonEdit.Rendering // 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); } } diff --git a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj index ba8057a8df..21f22d6ddd 100644 --- a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj +++ b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj @@ -167,7 +167,6 @@ - ToolTipService.cs diff --git a/src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs b/src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs index 757ac459a4..a42f4b61c7 100644 --- a/src/Main/Base/Project/Src/Editor/CodeCompletion/IInsightWindow.cs +++ b/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 { diff --git a/src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs b/src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs index 402e191f58..cf0c5e581c 100644 --- a/src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs +++ b/src/Main/Base/Project/Src/Editor/ISyntaxHighlighter.cs @@ -52,6 +52,16 @@ namespace ICSharpCode.SharpDevelop.Editor /// (e.g. semantic highlighting). /// void InvalidateAll(); + + /// + /// Gets the document lines that are currently visible in the editor. + /// + IEnumerable GetVisibleDocumentLines(); + + /// + /// Raised when the set of visible document lines has changed. + /// + event EventHandler VisibleDocumentLinesChanged; } public static class SyntaxHighligherKnownSpanNames diff --git a/src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs b/src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs deleted file mode 100644 index e84a61fca0..0000000000 --- a/src/Main/Base/Project/Src/Editor/TextChangeEventArgs.cs +++ /dev/null @@ -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 -{ - /// - /// Describes a change of the document text. - /// This class is thread-safe. - /// - public class TextChangeEventArgs : EventArgs - { - /// - /// The offset at which the change occurs. - /// - public int Offset { get; private set; } - - /// - /// The text that was inserted. - /// - public string RemovedText { get; private set; } - - /// - /// The number of characters removed. - /// - public int RemovalLength { - get { return RemovedText.Length; } - } - - /// - /// The text that was inserted. - /// - public string InsertedText { get; private set; } - - /// - /// The number of characters inserted. - /// - public int InsertionLength { - get { return InsertedText.Length; } - } - - /// - /// Creates a new TextChangeEventArgs object. - /// - public TextChangeEventArgs(int offset, string removedText, string insertedText) - { - this.Offset = offset; - this.RemovedText = removedText ?? string.Empty; - this.InsertedText = insertedText ?? string.Empty; - } - } -}