// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using ICSharpCode.NRefactory.Editor; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Utils; using SpanStack = ICSharpCode.AvalonEdit.Utils.ImmutableStack; namespace ICSharpCode.AvalonEdit.Highlighting { /// /// This class can syntax-highlight a document. /// It automatically manages invalidating the highlighting when the document changes. /// public class DocumentHighlighter : ILineTracker, IHighlighter { /// /// Stores the span state at the end of each line. /// storedSpanStacks[0] = state at beginning of document /// storedSpanStacks[i] = state after line i /// readonly CompressingTreeList storedSpanStacks = new CompressingTreeList(object.ReferenceEquals); readonly CompressingTreeList isValid = new CompressingTreeList((a, b) => a == b); readonly IDocument document; readonly IHighlightingDefinition definition; readonly HighlightingEngine engine; readonly WeakLineTracker weakLineTracker; bool isHighlighting; bool isInHighlightingGroup; bool isDisposed; /// /// Gets the document that this DocumentHighlighter is highlighting. /// public IDocument Document { get { return document; } } /// /// Creates a new DocumentHighlighter instance. /// public DocumentHighlighter(TextDocument document, IHighlightingDefinition definition) { if (document == null) throw new ArgumentNullException("document"); if (definition == null) throw new ArgumentNullException("definition"); this.document = document; this.definition = definition; this.engine = new HighlightingEngine(definition.MainRuleSet); document.VerifyAccess(); weakLineTracker = WeakLineTracker.Register(document, this); InvalidateHighlighting(); } #if NREFACTORY /// /// Creates a new DocumentHighlighter instance. /// public DocumentHighlighter(ReadOnlyDocument document, IHighlightingDefinition definition) { if (document == null) throw new ArgumentNullException("document"); if (definition == null) throw new ArgumentNullException("definition"); this.document = document; this.definition = definition; this.engine = new HighlightingEngine(definition.MainRuleSet); InvalidateHighlighting(); } #endif /// /// Disposes the document highlighter. /// public void Dispose() { if (weakLineTracker != null) weakLineTracker.Deregister(); isDisposed = true; } void ILineTracker.BeforeRemoveLine(DocumentLine line) { CheckIsHighlighting(); int number = line.LineNumber; storedSpanStacks.RemoveAt(number); isValid.RemoveAt(number); if (number < isValid.Count) { isValid[number] = false; if (number < firstInvalidLine) firstInvalidLine = number; } } void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength) { CheckIsHighlighting(); int number = line.LineNumber; isValid[number] = false; if (number < firstInvalidLine) firstInvalidLine = number; } void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) { CheckIsHighlighting(); Debug.Assert(insertionPos.LineNumber + 1 == newLine.LineNumber); int lineNumber = newLine.LineNumber; storedSpanStacks.Insert(lineNumber, null); isValid.Insert(lineNumber, false); if (lineNumber < firstInvalidLine) firstInvalidLine = lineNumber; } void ILineTracker.RebuildDocument() { InvalidateHighlighting(); } void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) { } ImmutableStack initialSpanStack = SpanStack.Empty; /// /// Gets/sets the the initial span stack of the document. Default value is . /// public ImmutableStack InitialSpanStack { get { return initialSpanStack; } set { initialSpanStack = value ?? SpanStack.Empty; InvalidateHighlighting(); } } /// /// Invalidates all stored highlighting info. /// When the document changes, the highlighting is invalidated automatically, this method /// needs to be called only when there are changes to the highlighting rule set. /// public void InvalidateHighlighting() { CheckIsHighlighting(); storedSpanStacks.Clear(); storedSpanStacks.Add(initialSpanStack); storedSpanStacks.InsertRange(1, document.LineCount, null); isValid.Clear(); isValid.Add(true); isValid.InsertRange(1, document.LineCount, false); firstInvalidLine = 1; } int firstInvalidLine; /// public HighlightedLine HighlightLine(int lineNumber) { ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 1, document.LineCount); CheckIsHighlighting(); isHighlighting = true; try { HighlightUpTo(lineNumber - 1); IDocumentLine line = document.GetLineByNumber(lineNumber); HighlightedLine result = engine.HighlightLine(document, line); UpdateTreeList(lineNumber); return result; } finally { isHighlighting = false; } } /// /// Gets the span stack at the end of the specified line. /// -> GetSpanStack(1) returns the spans at the start of the second line. /// /// /// GetSpanStack(0) is valid and will return . /// The elements are returned in inside-out order (first element of result enumerable is the color of the innermost span). /// public SpanStack GetSpanStack(int lineNumber) { ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); if (firstInvalidLine <= lineNumber) { UpdateHighlightingState(lineNumber); } return storedSpanStacks[lineNumber]; } /// public IEnumerable GetColorStack(int lineNumber) { return GetSpanStack(lineNumber).Select(s => s.SpanColor).Where(s => s != null); } void CheckIsHighlighting() { if (isDisposed) { throw new ObjectDisposedException("DocumentHighlighter"); } if (isHighlighting) { throw new InvalidOperationException("Invalid call - a highlighting operation is currently running."); } } /// public void UpdateHighlightingState(int lineNumber) { CheckIsHighlighting(); isHighlighting = true; try { HighlightUpTo(lineNumber); } finally { isHighlighting = false; } } /// /// Sets the engine's CurrentSpanStack to the end of the target line. /// Updates the span stack for all lines up to (and including) the target line, if necessary. /// void HighlightUpTo(int targetLineNumber) { for (int currentLine = 0; currentLine <= targetLineNumber; currentLine++) { if (firstInvalidLine > currentLine) { // (this branch is always taken on the first loop iteration, as firstInvalidLine > 0) if (firstInvalidLine <= targetLineNumber) { // Skip valid lines to next invalid line: engine.CurrentSpanStack = storedSpanStacks[firstInvalidLine - 1]; currentLine = firstInvalidLine; } else { // Skip valid lines to target line: engine.CurrentSpanStack = storedSpanStacks[targetLineNumber]; break; } } Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[currentLine - 1])); engine.ScanLine(document, document.GetLineByNumber(currentLine)); UpdateTreeList(currentLine); } Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[targetLineNumber])); } void UpdateTreeList(int lineNumber) { if (!EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[lineNumber])) { isValid[lineNumber] = true; //Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack); storedSpanStacks[lineNumber] = engine.CurrentSpanStack; if (lineNumber + 1 < isValid.Count) { isValid[lineNumber + 1] = false; firstInvalidLine = lineNumber + 1; } else { firstInvalidLine = int.MaxValue; } if (lineNumber + 1 < document.LineCount) OnHighlightStateChanged(lineNumber + 1, lineNumber + 1); } else if (firstInvalidLine == lineNumber) { isValid[lineNumber] = true; firstInvalidLine = isValid.IndexOf(false); if (firstInvalidLine < 0) firstInvalidLine = int.MaxValue; } } static bool EqualSpanStacks(SpanStack a, SpanStack b) { // We must use value equality between the stacks because HighlightingColorizer.OnHighlightStateChanged // depends on the fact that equal input state + unchanged line contents produce equal output state. if (a == b) return true; if (a == null || b == null) return false; while (!a.IsEmpty && !b.IsEmpty) { if (a.Peek() != b.Peek()) return false; a = a.Pop(); b = b.Pop(); if (a == b) return true; } return a.IsEmpty && b.IsEmpty; } /// public event HighlightingStateChangedEventHandler HighlightingStateChanged; /// /// Is called when the highlighting state at the end of the specified line has changed. /// /// This callback must not call HighlightLine or InvalidateHighlighting. /// It may call GetSpanStack, but only for the changed line and lines above. /// This method must not modify the document. protected virtual void OnHighlightStateChanged(int fromLineNumber, int toLineNumber) { if (HighlightingStateChanged != null) HighlightingStateChanged(fromLineNumber, toLineNumber); } /// 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); } } }