mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
12 KiB
357 lines
12 KiB
// 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<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>; |
|
|
|
namespace ICSharpCode.AvalonEdit.Highlighting |
|
{ |
|
/// <summary> |
|
/// This class can syntax-highlight a document. |
|
/// It automatically manages invalidating the highlighting when the document changes. |
|
/// </summary> |
|
public class DocumentHighlighter : ILineTracker, IHighlighter |
|
{ |
|
/// <summary> |
|
/// Stores the span state at the end of each line. |
|
/// storedSpanStacks[0] = state at beginning of document |
|
/// storedSpanStacks[i] = state after line i |
|
/// </summary> |
|
readonly CompressingTreeList<SpanStack> storedSpanStacks = new CompressingTreeList<SpanStack>(object.ReferenceEquals); |
|
readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b); |
|
readonly IDocument document; |
|
readonly IHighlightingDefinition definition; |
|
readonly HighlightingEngine engine; |
|
readonly WeakLineTracker weakLineTracker; |
|
bool isHighlighting; |
|
bool isInHighlightingGroup; |
|
bool isDisposed; |
|
|
|
/// <summary> |
|
/// Gets the document that this DocumentHighlighter is highlighting. |
|
/// </summary> |
|
public IDocument Document { |
|
get { return document; } |
|
} |
|
|
|
/// <summary> |
|
/// Creates a new DocumentHighlighter instance. |
|
/// </summary> |
|
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); |
|
InvalidateSpanStacks(); |
|
} |
|
|
|
#if NREFACTORY |
|
/// <summary> |
|
/// Creates a new DocumentHighlighter instance. |
|
/// </summary> |
|
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 |
|
|
|
/// <summary> |
|
/// Disposes the document highlighter. |
|
/// </summary> |
|
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() |
|
{ |
|
InvalidateSpanStacks(); |
|
} |
|
|
|
void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) |
|
{ |
|
} |
|
|
|
ImmutableStack<HighlightingSpan> initialSpanStack = SpanStack.Empty; |
|
|
|
/// <summary> |
|
/// Gets/sets the the initial span stack of the document. Default value is <see cref="SpanStack.Empty" />. |
|
/// </summary> |
|
public ImmutableStack<HighlightingSpan> InitialSpanStack { |
|
get { return initialSpanStack; } |
|
set { |
|
initialSpanStack = value ?? SpanStack.Empty; |
|
InvalidateHighlighting(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public void InvalidateHighlighting() |
|
{ |
|
InvalidateSpanStacks(); |
|
OnHighlightStateChanged(1, document.LineCount); // force a redraw with the new highlighting |
|
} |
|
|
|
/// <summary> |
|
/// Invalidates stored highlighting info, but does not raise the HighlightingStateChanged event. |
|
/// </summary> |
|
void InvalidateSpanStacks() |
|
{ |
|
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; |
|
|
|
/// <inheritdoc/> |
|
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; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets the span stack at the end of the specified line. |
|
/// -> GetSpanStack(1) returns the spans at the start of the second line. |
|
/// </summary> |
|
/// <remarks> |
|
/// GetSpanStack(0) is valid and will return <see cref="InitialSpanStack"/>. |
|
/// The elements are returned in inside-out order (first element of result enumerable is the color of the innermost span). |
|
/// </remarks> |
|
public SpanStack GetSpanStack(int lineNumber) |
|
{ |
|
ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); |
|
if (firstInvalidLine <= lineNumber) { |
|
UpdateHighlightingState(lineNumber); |
|
} |
|
return storedSpanStacks[lineNumber]; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public IEnumerable<HighlightingColor> 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."); |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void UpdateHighlightingState(int lineNumber) |
|
{ |
|
CheckIsHighlighting(); |
|
isHighlighting = true; |
|
try { |
|
HighlightUpTo(lineNumber); |
|
} finally { |
|
isHighlighting = false; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
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; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public event HighlightingStateChangedEventHandler HighlightingStateChanged; |
|
|
|
/// <summary> |
|
/// Is called when the highlighting state at the end of the specified line has changed. |
|
/// </summary> |
|
/// <remarks>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.</remarks> |
|
protected virtual void OnHighlightStateChanged(int fromLineNumber, int toLineNumber) |
|
{ |
|
if (HighlightingStateChanged != null) |
|
HighlightingStateChanged(fromLineNumber, toLineNumber); |
|
} |
|
|
|
/// <inheritdoc/> |
|
public HighlightingColor DefaultTextColor { |
|
get { return null; } |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void BeginHighlighting() |
|
{ |
|
if (isInHighlightingGroup) |
|
throw new InvalidOperationException("Highlighting group is already open"); |
|
isInHighlightingGroup = true; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public void EndHighlighting() |
|
{ |
|
if (!isInHighlightingGroup) |
|
throw new InvalidOperationException("Highlighting group is not open"); |
|
isInHighlightingGroup = false; |
|
} |
|
|
|
/// <inheritdoc/> |
|
public HighlightingColor GetNamedColor(string name) |
|
{ |
|
return definition.GetNamedColor(name); |
|
} |
|
} |
|
}
|
|
|