// 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.Globalization;
using System.IO;
using System.Linq;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using ICSharpCode.NRefactory.Editor;
namespace ICSharpCode.AvalonEdit.Highlighting
{
///
/// Represents a highlighted document line.
///
public class HighlightedLine
{
///
/// Creates a new HighlightedLine instance.
///
public HighlightedLine(IDocument document, IDocumentLine documentLine)
{
if (document == null)
throw new ArgumentNullException("document");
//if (!document.Lines.Contains(documentLine))
// throw new ArgumentException("Line is null or not part of document");
this.Document = document;
this.DocumentLine = documentLine;
this.Sections = new NullSafeCollection();
}
///
/// Gets the document associated with this HighlightedLine.
///
public IDocument Document { get; private set; }
///
/// Gets the document line associated with this HighlightedLine.
///
public IDocumentLine DocumentLine { get; private set; }
///
/// Gets the highlighted sections.
/// The sections are not overlapping, but they may be nested.
/// In that case, outer sections come in the list before inner sections.
/// The sections are sorted by start offset.
///
public IList Sections { get; private set; }
///
/// Validates that the sections are sorted correctly, and that they are not overlapping.
///
///
public void ValidateInvariants()
{
var line = this;
int lineStartOffset = line.DocumentLine.Offset;
int lineEndOffset = line.DocumentLine.EndOffset;
for (int i = 0; i < line.Sections.Count; i++) {
HighlightedSection s1 = line.Sections[i];
if (s1.Offset < lineStartOffset || s1.Length < 0 || s1.Offset + s1.Length > lineEndOffset)
throw new InvalidOperationException("Section is outside line bounds");
for (int j = i + 1; j < line.Sections.Count; j++) {
HighlightedSection s2 = line.Sections[j];
if (s2.Offset >= s1.Offset + s1.Length) {
// s2 is after s1
} else if (s2.Offset >= s1.Offset && s2.Offset + s2.Length <= s1.Offset + s1.Length) {
// s2 is nested within s1
} else {
throw new InvalidOperationException("Sections are overlapping or incorrectly sorted.");
}
}
}
}
#region Merge
///
/// Merges the additional line into this line.
///
public void MergeWith(HighlightedLine additionalLine)
{
if (additionalLine == null)
return;
#if DEBUG
ValidateInvariants();
additionalLine.ValidateInvariants();
#endif
int pos = 0;
Stack activeSectionEndOffsets = new Stack();
int lineEndOffset = this.DocumentLine.EndOffset;
activeSectionEndOffsets.Push(lineEndOffset);
foreach (HighlightedSection newSection in additionalLine.Sections) {
int newSectionStart = newSection.Offset;
// Track the existing sections using the stack, up to the point where
// we need to insert the first part of the newSection
while (pos < this.Sections.Count) {
HighlightedSection s = this.Sections[pos];
if (newSection.Offset < s.Offset)
break;
while (s.Offset > activeSectionEndOffsets.Peek()) {
activeSectionEndOffsets.Pop();
}
activeSectionEndOffsets.Push(s.Offset + s.Length);
pos++;
}
// Now insert the new section
// Create a copy of the stack so that we can track the sections we traverse
// during the insertion process:
Stack insertionStack = new Stack(activeSectionEndOffsets.Reverse());
// The stack enumerator reverses the order of the elements, so we call Reverse() to restore
// the original order.
int i;
for (i = pos; i < this.Sections.Count; i++) {
HighlightedSection s = this.Sections[i];
if (newSection.Offset + newSection.Length <= s.Offset)
break;
// Insert a segment in front of s:
Insert(ref i, ref newSectionStart, s.Offset, newSection.Color, insertionStack);
while (s.Offset > insertionStack.Peek()) {
insertionStack.Pop();
}
insertionStack.Push(s.Offset + s.Length);
}
Insert(ref i, ref newSectionStart, newSection.Offset + newSection.Length, newSection.Color, insertionStack);
}
#if DEBUG
ValidateInvariants();
#endif
}
void Insert(ref int pos, ref int newSectionStart, int insertionEndPos, HighlightingColor color, Stack insertionStack)
{
if (newSectionStart >= insertionEndPos) {
// nothing to insert here
return;
}
while (insertionStack.Peek() <= newSectionStart) {
insertionStack.Pop();
}
while (insertionStack.Peek() < insertionEndPos) {
int end = insertionStack.Pop();
// insert the portion from newSectionStart to end
if (end > newSectionStart) {
this.Sections.Insert(pos++, new HighlightedSection {
Offset = newSectionStart,
Length = end - newSectionStart,
Color = color
});
newSectionStart = end;
}
}
if (insertionEndPos > newSectionStart) {
this.Sections.Insert(pos++, new HighlightedSection {
Offset = newSectionStart,
Length = insertionEndPos - newSectionStart,
Color = color
});
newSectionStart = insertionEndPos;
}
}
#endregion
#region WriteTo / ToHtml
sealed class HtmlElement : IComparable
{
internal readonly int Offset;
internal readonly int Nesting;
internal readonly bool IsEnd;
internal readonly HighlightingColor Color;
public HtmlElement(int offset, int nesting, bool isEnd, HighlightingColor color)
{
this.Offset = offset;
this.Nesting = nesting;
this.IsEnd = isEnd;
this.Color = color;
}
public int CompareTo(HtmlElement other)
{
int r = Offset.CompareTo(other.Offset);
if (r != 0)
return r;
if (IsEnd != other.IsEnd) {
if (IsEnd)
return -1;
else
return 1;
} else {
if (IsEnd)
return other.Nesting.CompareTo(Nesting);
else
return Nesting.CompareTo(other.Nesting);
}
}
}
///
/// Writes the highlighted line to the RichTextWriter.
///
internal void WriteTo(RichTextWriter writer)
{
int startOffset = this.DocumentLine.Offset;
WriteTo(writer, startOffset, startOffset + this.DocumentLine.Length);
}
///
/// Writes a part of the highlighted line to the RichTextWriter.
///
internal void WriteTo(RichTextWriter writer, int startOffset, int endOffset)
{
if (writer == null)
throw new ArgumentNullException("writer");
int documentLineStartOffset = this.DocumentLine.Offset;
int documentLineEndOffset = documentLineStartOffset + this.DocumentLine.Length;
if (startOffset < documentLineStartOffset || startOffset > documentLineEndOffset)
throw new ArgumentOutOfRangeException("startOffset", startOffset, "Value must be between " + documentLineStartOffset + " and " + documentLineEndOffset);
if (endOffset < startOffset || endOffset > documentLineEndOffset)
throw new ArgumentOutOfRangeException("endOffset", endOffset, "Value must be between startOffset and " + documentLineEndOffset);
ISegment requestedSegment = new SimpleSegment(startOffset, endOffset - startOffset);
List elements = new List();
for (int i = 0; i < this.Sections.Count; i++) {
HighlightedSection s = this.Sections[i];
if (SimpleSegment.GetOverlap(s, requestedSegment).Length > 0) {
elements.Add(new HtmlElement(s.Offset, i, false, s.Color));
elements.Add(new HtmlElement(s.Offset + s.Length, i, true, s.Color));
}
}
elements.Sort();
IDocument document = this.Document;
int textOffset = startOffset;
foreach (HtmlElement e in elements) {
int newOffset = Math.Min(e.Offset, endOffset);
if (newOffset > startOffset) {
document.WriteTextTo(writer, textOffset, newOffset - textOffset);
}
textOffset = Math.Max(textOffset, newOffset);
if (e.IsEnd)
writer.EndSpan();
else
writer.BeginSpan(e.Color);
}
document.WriteTextTo(writer, textOffset, endOffset - textOffset);
}
///
/// Produces HTML code for the line, with <span class="colorName"> tags.
///
public string ToHtml(HtmlOptions options = null)
{
StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture);
using (var htmlWriter = new HtmlRichTextWriter(stringWriter, options)) {
WriteTo(htmlWriter);
}
return stringWriter.ToString();
}
///
/// Produces HTML code for a section of the line, with <span class="colorName"> tags.
///
public string ToHtml(int startOffset, int endOffset, HtmlOptions options = null)
{
StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture);
using (var htmlWriter = new HtmlRichTextWriter(stringWriter, options)) {
WriteTo(htmlWriter, startOffset, endOffset);
}
return stringWriter.ToString();
}
///
public override string ToString()
{
return "[" + GetType().Name + " " + ToHtml() + "]";
}
#endregion
///
/// Creates a that stores the text and highlighting of this line.
///
[Obsolete("Use ToRichText() instead")]
public HighlightedInlineBuilder ToInlineBuilder()
{
HighlightedInlineBuilder builder = new HighlightedInlineBuilder(Document.GetText(DocumentLine));
int startOffset = DocumentLine.Offset;
foreach (HighlightedSection section in Sections) {
builder.SetHighlighting(section.Offset - startOffset, section.Length, section.Color);
}
return builder;
}
///
/// Creates a that stores the highlighting of this line.
///
public RichTextModel ToRichTextModel()
{
var builder = new RichTextModel();
int startOffset = DocumentLine.Offset;
foreach (HighlightedSection section in Sections) {
builder.ApplyHighlighting(section.Offset - startOffset, section.Length, section.Color);
}
return builder;
}
///
/// Creates a that stores the text and highlighting of this line.
///
public RichText ToRichText()
{
return new RichText(Document.GetText(DocumentLine), ToRichTextModel());
}
}
}