// 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.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Folding
{
///
/// Stores a list of foldings for a specific TextView and TextDocument.
///
public class FoldingManager : IWeakEventListener
{
internal readonly TextDocument document;
internal readonly List textViews = new List();
readonly TextSegmentCollection foldings;
bool isFirstUpdate = true;
#region Constructor
///
/// Creates a new FoldingManager instance.
///
public FoldingManager(TextDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
this.document = document;
this.foldings = new TextSegmentCollection();
document.VerifyAccess();
TextDocumentWeakEventManager.Changed.AddListener(document, this);
}
#endregion
#region 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
///
/// Creates a folding for the specified text section.
///
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;
}
///
/// Removes a folding section from this manager.
///
public void RemoveFolding(FoldingSection fs)
{
if (fs == null)
throw new ArgumentNullException("fs");
fs.IsFolded = false;
foldings.Remove(fs);
Redraw(fs);
}
///
/// Removes all folding sections.
///
public void Clear()
{
document.VerifyAccess();
foreach (FoldingSection s in foldings)
s.IsFolded = false;
foldings.Clear();
Redraw();
}
#endregion
#region Get...Folding
///
/// 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.
///
public IEnumerable AllFoldings {
get { return foldings; }
}
///
/// Gets the first offset greater or equal to where a folded folding starts.
/// Returns -1 if there are no foldings after .
///
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;
}
///
/// Gets the first folding with a greater or equal to
/// .
/// Returns null if there are no foldings after .
///
public FoldingSection GetNextFolding(int startOffset)
{
// TODO: returns the longest folding instead of any folding at the first position after startOffset
return foldings.FindFirstSegmentWithStartAfter(startOffset);
}
///
/// Gets all foldings that start exactly at .
///
public ReadOnlyCollection GetFoldingsAt(int startOffset)
{
List result = new List();
FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
while (fs != null && fs.StartOffset == startOffset) {
result.Add(fs);
fs = foldings.GetNextSegment(fs);
}
return result.AsReadOnly();
}
///
/// Gets all foldings that contain .
///
public ReadOnlyCollection GetFoldingsContaining(int offset)
{
return foldings.FindSegmentsContaining(offset);
}
#endregion
#region UpdateFoldings
///
/// Updates the foldings in this using the given new foldings.
/// This method will try to detect which new foldings correspond to which existing foldings; and will keep the state
/// () for existing foldings.
///
/// The new set of foldings. These must be sorted by starting offset.
/// The first position of a parse error. Existing foldings starting after
/// this offset will be kept even if they don't appear in .
/// Use -1 for this parameter if there were no parse errors.
public void UpdateFoldings(IEnumerable 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
if (isFirstUpdate) {
section.IsFolded = newFolding.DefaultClosed;
isFirstUpdate = false;
}
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
///
/// 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.
///
/// The that manages the list of foldings inside the text area.
public static FoldingManager Install(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
return new FoldingManagerInstallation(textArea);
}
///
/// Uninstalls the folding manager.
///
/// The specified manager was not created using .
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
}
}