// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Folding
{
/// <summary>
/// Stores a list of foldings for a specific TextView and TextDocument.
/// </summary>
public class FoldingManager : IWeakEventListener
{
internal readonly TextDocument document;
internal readonly List<TextView> textViews = new List<TextView>();
readonly TextSegmentCollection<FoldingSection> foldings;
#region Constructor
/// <summary>
/// Creates a new FoldingManager instance.
/// </summary>
public FoldingManager(TextDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
this.document = document;
this.foldings = new TextSegmentCollection<FoldingSection>();
document.VerifyAccess();
TextDocumentWeakEventManager.Changed.AddListener(document, this);
}
/// <summary>
/// Creates a new FoldingManager instance.
/// </summary>
[Obsolete("Use the (TextDocument) constructor instead.")]
public FoldingManager(TextView textView, TextDocument document)
: this(document)
{
}
#endregion
#region ReceiveWeakEvent
/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
OnDocumentChanged((DocumentChangeEventArgs)e);
return true;
}
return false;
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
return ReceiveWeakEvent(managerType, sender, e);
}
void OnDocumentChanged(DocumentChangeEventArgs e)
{
foldings.UpdateOffsets(e);
int newEndOffset = e.Offset + e.InsertionLength;
// extend end offset to the end of the line (including delimiter)
var endLine = document.GetLineByOffset(newEndOffset);
newEndOffset = endLine.Offset + endLine.TotalLength;
foreach (var affectedFolding in foldings.FindOverlappingSegments(e.Offset, newEndOffset - e.Offset)) {
if (affectedFolding.Length == 0) {
RemoveFolding(affectedFolding);
} else {
affectedFolding.ValidateCollapsedLineSections();
}
}
}
#endregion
#region Manage TextViews
internal void AddToTextView(TextView textView)
{
if (textView == null || textViews.Contains(textView))
throw new ArgumentException();
textViews.Add(textView);
foreach (FoldingSection fs in foldings) {
if (fs.collapsedSections != null) {
Array.Resize(ref fs.collapsedSections, textViews.Count);
fs.ValidateCollapsedLineSections();
}
}
}
internal void RemoveFromTextView(TextView textView)
{
int pos = textViews.IndexOf(textView);
if (pos < 0)
throw new ArgumentException();
textViews.RemoveAt(pos);
foreach (FoldingSection fs in foldings) {
if (fs.collapsedSections != null) {
var c = new CollapsedLineSection[textViews.Count];
Array.Copy(fs.collapsedSections, 0, c, 0, pos);
fs.collapsedSections[pos].Uncollapse();
Array.Copy(fs.collapsedSections, pos + 1, c, pos, c.Length - pos);
fs.collapsedSections = c;
}
}
}
internal void Redraw()
{
foreach (TextView textView in textViews)
textView.Redraw();
}
internal void Redraw(FoldingSection fs)
{
foreach (TextView textView in textViews)
textView.Redraw(fs);
}
#endregion
#region Create / Remove / Clear
/// <summary>
/// Creates a folding for the specified text section.
/// </summary>
public FoldingSection CreateFolding(int startOffset, int endOffset)
{
if (startOffset >= endOffset)
throw new ArgumentException("startOffset must be less than endOffset");
if (startOffset < 0 || endOffset > document.TextLength)
throw new ArgumentException("Folding must be within document boundary");
FoldingSection fs = new FoldingSection(this, startOffset, endOffset);
foldings.Add(fs);
Redraw(fs);
return fs;
}
/// <summary>
/// Removes a folding section from this manager.
/// </summary>
public void RemoveFolding(FoldingSection fs)
{
if (fs == null)
throw new ArgumentNullException("fs");
fs.IsFolded = false;
foldings.Remove(fs);
Redraw(fs);
}
/// <summary>
/// Removes all folding sections.
/// </summary>
public void Clear()
{
document.VerifyAccess();
foreach (FoldingSection s in foldings)
s.IsFolded = false;
foldings.Clear();
Redraw();
}
#endregion
#region Get...Folding
/// <summary>
/// Gets all foldings in this manager.
/// The foldings are returned sorted by start offset;
/// for multiple foldings at the same offset the order is undefined.
/// </summary>
public IEnumerable<FoldingSection> AllFoldings {
get { return foldings; }
}
/// <summary>
/// Gets the first offset greater or equal to <paramref name="startOffset"/> where a folded folding starts.
/// Returns -1 if there are no foldings after <paramref name="startOffset"/>.
/// </summary>
public int GetNextFoldedFoldingStart(int startOffset)
{
FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
while (fs != null && !fs.IsFolded)
fs = foldings.GetNextSegment(fs);
return fs != null ? fs.StartOffset : -1;
}
/// <summary>
/// Gets the first folding with a <see cref="TextSegment.StartOffset"/> greater or equal to
/// <paramref name="startOffset"/>.
/// Returns null if there are no foldings after <paramref name="startOffset"/>.
/// </summary>
public FoldingSection GetNextFolding(int startOffset)
{
// TODO: returns the longest folding instead of any folding at the first position after startOffset
return foldings.FindFirstSegmentWithStartAfter(startOffset);
}
/// <summary>
/// Gets all foldings that start exactly at <paramref name="startOffset"/>.
/// </summary>
public ReadOnlyCollection<FoldingSection> GetFoldingsAt(int startOffset)
{
List<FoldingSection> result = new List<FoldingSection>();
FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
while (fs != null && fs.StartOffset == startOffset) {
result.Add(fs);
fs = foldings.GetNextSegment(fs);
}
return result.AsReadOnly();
}
/// <summary>
/// Gets all foldings that contain <paramref name="offset" />.
/// </summary>
public ReadOnlyCollection<FoldingSection> GetFoldingsContaining(int offset)
{
return foldings.FindSegmentsContaining(offset);
}
#endregion
#region UpdateFoldings
/// <summary>
/// Updates the foldings in this <see cref="FoldingManager"/> using the given new foldings.
/// This method will try to detect which new foldings correspond to which existing foldings; and will keep the state
/// (<see cref="FoldingSection.IsFolded"/>) for existing foldings.
/// </summary>
/// <param name="newFoldings">The new set of foldings. These must be sorted by starting offset.</param>
/// <param name="firstErrorOffset">The first position of a parse error. Existing foldings starting after
/// this offset will be kept even if they don't appear in <paramref name="newFoldings"/>.
/// Use -1 for this parameter if there were no parse errors.</param>
public void UpdateFoldings(IEnumerable<NewFolding> newFoldings, int firstErrorOffset)
{
if (newFoldings == null)
throw new ArgumentNullException("newFoldings");
if (firstErrorOffset < 0)
firstErrorOffset = int.MaxValue;
var oldFoldings = this.AllFoldings.ToArray();
int oldFoldingIndex = 0;
int previousStartOffset = 0;
// merge new foldings into old foldings so that sections keep being collapsed
// both oldFoldings and newFoldings are sorted by start offset
foreach (NewFolding newFolding in newFoldings) {
// ensure newFoldings are sorted correctly
if (newFolding.StartOffset < previousStartOffset)
throw new ArgumentException("newFoldings must be sorted by start offset");
previousStartOffset = newFolding.StartOffset;
int startOffset = newFolding.StartOffset.CoerceValue(0, document.TextLength);
int endOffset = newFolding.EndOffset.CoerceValue(0, document.TextLength);
if (newFolding.StartOffset == newFolding.EndOffset)
continue; // ignore zero-length foldings
// remove old foldings that were skipped
while (oldFoldingIndex < oldFoldings.Length && newFolding.StartOffset > oldFoldings[oldFoldingIndex].StartOffset) {
this.RemoveFolding(oldFoldings[oldFoldingIndex++]);
}
FoldingSection section;
// reuse current folding if its matching:
if (oldFoldingIndex < oldFoldings.Length && newFolding.StartOffset == oldFoldings[oldFoldingIndex].StartOffset) {
section = oldFoldings[oldFoldingIndex++];
section.Length = newFolding.EndOffset - newFolding.StartOffset;
} else {
// no matching current folding; create a new one:
section = this.CreateFolding(newFolding.StartOffset, newFolding.EndOffset);
// auto-close #regions only when opening the document
section.IsFolded = newFolding.DefaultClosed;
section.Tag = newFolding;
}
section.Title = newFolding.Name;
}
// remove all outstanding old foldings:
while (oldFoldingIndex < oldFoldings.Length) {
FoldingSection oldSection = oldFoldings[oldFoldingIndex++];
if (oldSection.StartOffset >= firstErrorOffset)
break;
this.RemoveFolding(oldSection);
}
}
#endregion
#region Install
/// <summary>
/// Adds Folding support to the specified text area.
/// Warning: The folding manager is only valid for the text area's current document. The folding manager
/// must be uninstalled before the text area is bound to a different document.
/// </summary>
/// <returns>The <see cref="FoldingManager"/> that manages the list of foldings inside the text area.</returns>
public static FoldingManager Install(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
return new FoldingManagerInstallation(textArea);
}
/// <summary>
/// Uninstalls the folding manager.
/// </summary>
/// <exception cref="ArgumentException">The specified manager was not created using <see cref="Install"/>.</exception>
public static void Uninstall(FoldingManager manager)
{
if (manager == null)
throw new ArgumentNullException("manager");
FoldingManagerInstallation installation = manager as FoldingManagerInstallation;
if (installation != null) {
installation.Uninstall();
} else {
throw new ArgumentException("FoldingManager was not created using FoldingManager.Install");
}
}
sealed class FoldingManagerInstallation : FoldingManager
{
TextArea textArea;
FoldingMargin margin;
FoldingElementGenerator generator;
public FoldingManagerInstallation(TextArea textArea) : base(textArea.Document)
{
this.textArea = textArea;
margin = new FoldingMargin() { FoldingManager = this };
generator = new FoldingElementGenerator() { FoldingManager = this };
textArea.LeftMargins.Add(margin);
textArea.TextView.Services.AddService(typeof(FoldingManager), this);
// HACK: folding only works correctly when it has highest priority
textArea.TextView.ElementGenerators.Insert(0, generator);
textArea.Caret.PositionChanged += textArea_Caret_PositionChanged;
}
/*
void DemoMode()
{
foldingGenerator = new FoldingElementGenerator() { FoldingManager = fm };
foldingMargin = new FoldingMargin { FoldingManager = fm };
foldingMarginBorder = new Border {
Child = foldingMargin,
Background = new LinearGradientBrush(Colors.White, Colors.Transparent, 0)
};
foldingMarginBorder.SizeChanged += UpdateTextViewClip;
textEditor.TextArea.TextView.ElementGenerators.Add(foldingGenerator);
textEditor.TextArea.LeftMargins.Add(foldingMarginBorder);
}
void UpdateTextViewClip(object sender, SizeChangedEventArgs e)
{
textEditor.TextArea.TextView.Clip = new RectangleGeometry(
new Rect(-foldingMarginBorder.ActualWidth,
0,
textEditor.TextArea.TextView.ActualWidth + foldingMarginBorder.ActualWidth,
textEditor.TextArea.TextView.ActualHeight));
}
*/
public void Uninstall()
{
Clear();
if (textArea != null) {
textArea.Caret.PositionChanged -= textArea_Caret_PositionChanged;
textArea.LeftMargins.Remove(margin);
textArea.TextView.ElementGenerators.Remove(generator);
textArea.TextView.Services.RemoveService(typeof(FoldingManager));
margin = null;
generator = null;
textArea = null;
}
}
void textArea_Caret_PositionChanged(object sender, EventArgs e)
{
// Expand Foldings when Caret is moved into them.
int caretOffset = textArea.Caret.Offset;
foreach (FoldingSection s in GetFoldingsContaining(caretOffset)) {
if (s.IsFolded && s.StartOffset < caretOffset && caretOffset < s.EndOffset) {
s.IsFolded = false;
}
}
}
}
#endregion
}
}