From c3bd806532d458597179d988b4646fe6a079c03d Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 5 Oct 2012 04:34:23 +0200 Subject: [PATCH] Use BeginHighlighting()/EndHighlighting() to improve performance of CSharpSemanticHighlighter. --- .../Project/Src/CSharpSemanticHighlighter.cs | 118 ++++++++++++------ .../Project/Src/Parser/CSharpSymbolSearch.cs | 2 + .../Project/Src/Parser/Parser.cs | 2 + .../Src/Refactoring/SearchForIssuesCommand.cs | 5 + .../XamlBinding/XamlSymbolSearch.cs | 2 + .../Project/Engine/SearchManager.cs | 2 + .../Highlighting/DocumentHighlighter.cs | 19 ++- .../Highlighting/HighlightingColorizer.cs | 40 +++++- .../Highlighting/IHighlighter.cs | 12 +- .../FindReferencesAndRenameHelper.cs | 2 + 10 files changed, 160 insertions(+), 44 deletions(-) diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs index ff70ebc12e..872548806b 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/CSharpSemanticHighlighter.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Windows.Threading; using CSharpBinding.Parser; +using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.Core; @@ -38,14 +39,17 @@ namespace CSharpBinding readonly HighlightingColor parameterModifierColor; readonly HighlightingColor inactiveCodeColor; - List invalidLines = new List(); - List cachedLines = new List(); + List invalidLines; + List cachedLines; bool hasCrashed; bool forceParseOnNextRefresh; + bool eventHandlersAreRegistered; + bool inHighlightingGroup; int lineNumber; HighlightedLine line; CSharpAstResolver resolver; + CSharpFullParseInformation parseInfo; bool isInAccessor; @@ -66,14 +70,30 @@ namespace CSharpBinding this.parameterModifierColor = highlighting.GetNamedColor("ParameterModifiers"); this.inactiveCodeColor = highlighting.GetNamedColor("InactiveCode"); - SD.ParserService.ParseInformationUpdated += ParserService_ParseInformationUpdated; - SD.ParserService.LoadSolutionProjectsThread.Finished += ParserService_LoadSolutionProjectsThreadEnded; + if (document is TextDocument && SD.MainThread.CheckAccess()) { + // Use the cache only for the live AvalonEdit document + // Highlighting in read-only documents (e.g. search results) does + // not need the cache as it does not need to highlight the same line multiple times + cachedLines = new List(); + // Line invalidation is only necessary for the live AvalonEdit document + invalidLines = new List(); + // Also, attach these event handlers only for real documents in the editor, + // we don't need them for the highlighting in search results etc. + SD.ParserService.ParseInformationUpdated += ParserService_ParseInformationUpdated; + SD.ParserService.LoadSolutionProjectsThread.Finished += ParserService_LoadSolutionProjectsThreadEnded; + eventHandlersAreRegistered = true; + } } public void Dispose() { - SD.ParserService.ParseInformationUpdated -= ParserService_ParseInformationUpdated; - SD.ParserService.LoadSolutionProjectsThread.Finished -= ParserService_LoadSolutionProjectsThreadEnded; + if (eventHandlersAreRegistered) { + SD.ParserService.ParseInformationUpdated -= ParserService_ParseInformationUpdated; + SD.ParserService.LoadSolutionProjectsThread.Finished -= ParserService_LoadSolutionProjectsThreadEnded; + eventHandlersAreRegistered = false; + } + this.resolver = null; + this.parseInfo = null; } #endregion @@ -196,34 +216,53 @@ namespace CSharpBinding } ITextSourceVersion newVersion = 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]; + if (cachedLines != 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; } - 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; } } - 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; + bool wasInHighlightingGroup = inHighlightingGroup; + if (!inHighlightingGroup) { + BeginHighlighting(); } - - CSharpFullParseInformation parseInfo; - if (forceParseOnNextRefresh) { - forceParseOnNextRefresh = false; - parseInfo = SD.ParserService.Parse(FileName.Create(document.FileName), document) as CSharpFullParseInformation; - } else { - parseInfo = SD.ParserService.GetCachedParseInformation(FileName.Create(document.FileName), document.Version) as CSharpFullParseInformation; + try { + return DoHighlightLine(lineNumber, documentLine, cachedLine, newVersion); + } finally { + line = null; + if (!wasInHighlightingGroup) + EndHighlighting(); + } + } + + HighlightedLine DoHighlightLine(int lineNumber, IDocumentLine documentLine, CachedLine cachedLine, ITextSourceVersion newVersion) + { + if (parseInfo == null) { + if (forceParseOnNextRefresh) { + forceParseOnNextRefresh = false; + parseInfo = SD.ParserService.Parse(FileName.Create(document.FileName), document) as CSharpFullParseInformation; + } else { + parseInfo = SD.ParserService.GetCachedParseInformation(FileName.Create(document.FileName), newVersion) as CSharpFullParseInformation; + } } if (parseInfo == null) { - if (!invalidLines.Contains(documentLine)) + if (invalidLines != null && !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. @@ -235,11 +274,12 @@ namespace CSharpBinding } } - var compilation = SD.ParserService.GetCompilationForFile(parseInfo.FileName); - this.resolver = parseInfo.GetResolver(compilation); + if (resolver == null) { + var compilation = SD.ParserService.GetCompilationForFile(parseInfo.FileName); + resolver = parseInfo.GetResolver(compilation); + } - HighlightedLine line = new HighlightedLine(document, documentLine); - this.line = line; + line = new HighlightedLine(document, documentLine); this.lineNumber = lineNumber; if (Debugger.IsAttached) { parseInfo.SyntaxTree.AcceptVisitor(this); @@ -251,10 +291,8 @@ namespace CSharpBinding throw new ApplicationException("Error highlighting line " + lineNumber, ex); } } - this.line = null; - this.resolver = null; //Debug.WriteLine("Semantic highlighting for line {0} - added {1} sections", lineNumber, line.Sections.Count); - if (document.Version != null) { + if (cachedLines != null && document.Version != null) { cachedLines.Add(new CachedLine(line, document.Version)); } return line; @@ -268,12 +306,22 @@ namespace CSharpBinding public void BeginHighlighting() { - + if (inHighlightingGroup) + throw new InvalidOperationException(); + inHighlightingGroup = true; + if (invalidLines == null) { + // if invalidation isn't available, we're forced to parse the file now + forceParseOnNextRefresh = true; + } } public void EndHighlighting() { - // use this event to remove cached lines which are no longer visible + inHighlightingGroup = false; + this.resolver = null; + this.parseInfo = null; + + // TODO use this to remove cached lines which are no longer visible // var visibleDocumentLines = new HashSet(syntaxHighlighter.GetVisibleDocumentLines()); // cachedLines.RemoveAll(c => !visibleDocumentLines.Contains(c.DocumentLine)); } diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/CSharpSymbolSearch.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/CSharpSymbolSearch.cs index da02599a73..6119f4fb71 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/CSharpSymbolSearch.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/CSharpSymbolSearch.cs @@ -106,6 +106,8 @@ namespace CSharpBinding if (document == null) { document = new ReadOnlyDocument(textSource, fileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) + highlighter.BeginHighlighting(); } Identifier identifier = node.GetChildByRole(Roles.Identifier); if (!identifier.IsNull) diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/Parser.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/Parser.cs index 4c565fa075..5b9c14fe16 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/Parser.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Parser/Parser.cs @@ -145,6 +145,8 @@ namespace CSharpBinding.Parser if (document == null) { document = new ReadOnlyDocument(fileContent, parseInfo.FileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) + highlighter.BeginHighlighting(); } var region = new DomRegion(parseInfo.FileName, node.StartLocation, node.EndLocation); int offset = document.GetOffset(node.StartLocation); diff --git a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Refactoring/SearchForIssuesCommand.cs b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Refactoring/SearchForIssuesCommand.cs index e0cb71a1db..7b067b8e80 100644 --- a/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Refactoring/SearchForIssuesCommand.cs +++ b/src/AddIns/BackendBindings/CSharpBinding/Project/Src/Refactoring/SearchForIssuesCommand.cs @@ -164,6 +164,9 @@ namespace CSharpBinding.Refactoring if (document == null) { document = new ReadOnlyDocument(fileContent, fileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) { + highlighter.BeginHighlighting(); + } } results.Add(SearchResultMatch.Create(document, issue.Start, issue.End, highlighter)); } @@ -247,6 +250,8 @@ namespace CSharpBinding.Refactoring } if (allIssues.Count > 0) { using (var highlighter = SD.EditorControlService.CreateHighlighter(document)) { + if (highlighter != null) + highlighter.BeginHighlighting(); return allIssues.Select(issue => SearchResultMatch.Create(document, issue.Start, issue.End, highlighter)).ToList(); } } else { diff --git a/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlSymbolSearch.cs b/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlSymbolSearch.cs index 44701f868f..e14552122c 100644 --- a/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlSymbolSearch.cs +++ b/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlSymbolSearch.cs @@ -94,6 +94,8 @@ namespace ICSharpCode.XamlBinding if (document == null) { document = new ReadOnlyDocument(textSource, fileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) + highlighter.BeginHighlighting(); } var result = resolver.Resolve(parseInfo, document.GetLocation(offset + entity.Name.Length / 2 + 1), compilation, cancellationToken); int length = entity.Name.Length; diff --git a/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs b/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs index da6afa5622..66cb585d60 100644 --- a/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs +++ b/src/AddIns/Misc/SearchAndReplace/Project/Engine/SearchManager.cs @@ -211,6 +211,8 @@ namespace SearchAndReplace if (document == null) { document = new ReadOnlyDocument(source, fileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) + highlighter.BeginHighlighting(); } var start = document.GetLocation(result.Offset); var end = document.GetLocation(result.Offset + result.Length); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs index ecdbf4d3f9..94ade5cd0f 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs @@ -31,6 +31,8 @@ namespace ICSharpCode.AvalonEdit.Highlighting readonly IHighlightingDefinition definition; readonly WeakLineTracker weakLineTracker; bool isHighlighting; + bool isInHighlightingGroup; + bool isDisposed; /// /// Gets the document that this DocumentHighlighter is highlighting. @@ -50,6 +52,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting throw new ArgumentNullException("definition"); this.document = document; this.definition = definition; + document.VerifyAccess(); weakLineTracker = WeakLineTracker.Register(document, this); InvalidateHighlighting(); } @@ -75,6 +78,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting { if (weakLineTracker != null) weakLineTracker.Deregister(); + isDisposed = true; } void ILineTracker.BeforeRemoveLine(DocumentLine line) @@ -197,6 +201,9 @@ namespace ICSharpCode.AvalonEdit.Highlighting void CheckIsHighlighting() { + if (isDisposed) { + throw new ObjectDisposedException("DocumentHighlighter"); + } if (isHighlighting) { throw new InvalidOperationException("Invalid call - a highlighting operation is currently running."); } @@ -505,20 +512,28 @@ namespace ICSharpCode.AvalonEdit.Highlighting } #endregion + /// public HighlightingColor DefaultTextColor { get { return null; } } + /// public void BeginHighlighting() { - + if (isInHighlightingGroup) + throw new InvalidOperationException("Highlighting group is already open"); + isInHighlightingGroup = true; } + /// public void EndHighlighting() { - + if (!isInHighlightingGroup) + throw new InvalidOperationException("Highlighting group is not open"); + isInHighlightingGroup = false; } + /// public HighlightingColor GetNamedColor(string name) { return definition.GetNamedColor(name); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs index ff8223de13..3ad17687a6 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs @@ -20,6 +20,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting readonly IHighlightingDefinition definition; TextView textView; IHighlighter highlighter; + bool isFixedHighlighter; /// /// Creates a new HighlightingColorizer instance. @@ -33,7 +34,9 @@ namespace ICSharpCode.AvalonEdit.Highlighting } /// - /// Creates a new HighlightingColorizer instance. + /// Creates a new HighlightingColorizer instance that uses a fixed highlighter instance. + /// The colorizer can only be used with text views that show the document for which + /// the highlighter was created. /// /// The highlighter to be used. public HighlightingColorizer(IHighlighter highlighter) @@ -41,6 +44,15 @@ namespace ICSharpCode.AvalonEdit.Highlighting if (highlighter == null) throw new ArgumentNullException("highlighter"); this.highlighter = highlighter; + this.isFixedHighlighter = true; + } + + /// + /// Creates a new HighlightingColorizer instance. + /// Derived classes using this constructor must override the method. + /// + protected HighlightingColorizer() + { } void textView_DocumentChanged(object sender, EventArgs e) @@ -61,7 +73,11 @@ namespace ICSharpCode.AvalonEdit.Highlighting // remove highlighter if it is registered if (textView.Services.GetService(typeof(IHighlighter)) == highlighter) textView.Services.RemoveService(typeof(IHighlighter)); - + if (!isFixedHighlighter) { + if (highlighter != null) + highlighter.Dispose(); + highlighter = null; + } } } @@ -72,8 +88,9 @@ namespace ICSharpCode.AvalonEdit.Highlighting protected virtual void RegisterServices(TextView textView) { if (textView.Document != null) { - highlighter = textView.Document != null ? CreateHighlighter(textView, textView.Document) : null; - if (highlighter != null) { + if (!isFixedHighlighter) + highlighter = textView.Document != null ? CreateHighlighter(textView, textView.Document) : null; + if (highlighter != null && highlighter.Document == textView.Document) { // add service only if it doesn't already exist if (textView.Services.GetService(typeof(IHighlighter)) == null) { textView.Services.AddService(typeof(IHighlighter), highlighter); @@ -88,7 +105,10 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// protected virtual IHighlighter CreateHighlighter(TextView textView, TextDocument document) { - return highlighter ?? new DocumentHighlighter(document, definition); + if (definition != null) + return new DocumentHighlighter(document, definition); + else + throw new NotSupportedException("Cannot create a highlighter because no IHighlightingDefinition was specified, and the CreateHighlighter() method was not overridden."); } /// @@ -101,6 +121,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting this.textView = textView; textView.DocumentChanged += textView_DocumentChanged; textView.VisualLineConstructionStarting += textView_VisualLineConstructionStarting; + textView.VisualLinesChanged += textView_VisualLinesChanged; RegisterServices(textView); } @@ -110,6 +131,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting DeregisterServices(textView); textView.DocumentChanged -= textView_DocumentChanged; textView.VisualLineConstructionStarting -= textView_VisualLineConstructionStarting; + textView.VisualLinesChanged -= textView_VisualLinesChanged; base.OnRemoveFromTextView(textView); this.textView = null; } @@ -122,11 +144,19 @@ namespace ICSharpCode.AvalonEdit.Highlighting // 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. lineNumberBeingColorized = e.FirstLineInView.LineNumber - 1; + highlighter.BeginHighlighting(); highlighter.UpdateHighlightingState(lineNumberBeingColorized); lineNumberBeingColorized = 0; } } + void textView_VisualLinesChanged(object sender, EventArgs e) + { + if (highlighter != null) { + highlighter.EndHighlighting(); + } + } + DocumentLine lastColorizedLine; /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs index 70c37ef26a..d88820dd28 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlighter.cs @@ -63,13 +63,21 @@ namespace ICSharpCode.AvalonEdit.Highlighting event HighlightingStateChangedEventHandler HighlightingStateChanged; /// - /// Opens a group of HighlightLine calls. + /// Opens a group of calls. + /// It is not necessary to call this method before calling , + /// however, doing so can make the highlighting much more performant in some cases + /// (e.g. the C# semantic highlighter in SharpDevelop will re-use the resolver within a highlighting group). /// + /// + /// The group is closed by either a or a call. + /// Nested groups are not allowed. + /// void BeginHighlighting(); /// - /// Closes the currently opened group of HighlightLine calls. + /// Closes the currently opened group of calls. /// + /// . void EndHighlighting(); /// diff --git a/src/Main/Base/Project/Src/Services/RefactoringService/FindReferencesAndRenameHelper.cs b/src/Main/Base/Project/Src/Services/RefactoringService/FindReferencesAndRenameHelper.cs index 40c23dd6db..3c7a0fa804 100644 --- a/src/Main/Base/Project/Src/Services/RefactoringService/FindReferencesAndRenameHelper.cs +++ b/src/Main/Base/Project/Src/Services/RefactoringService/FindReferencesAndRenameHelper.cs @@ -354,6 +354,8 @@ namespace ICSharpCode.SharpDevelop.Refactoring buffer = SD.FileService.GetFileContent(r.FileName); document = new ReadOnlyDocument(buffer, r.FileName); highlighter = SD.EditorControlService.CreateHighlighter(document); + if (highlighter != null) + highlighter.BeginHighlighting(); } var start = r.StartLocation; var end = r.EndLocation;