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.
388 lines
13 KiB
388 lines
13 KiB
// 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.collapsedSections[fs.collapsedSections.Length - 1] = fs.CollapseSection(textView); |
|
} |
|
} |
|
} |
|
|
|
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 |
|
} |
|
}
|
|
|