/AvalonEdit/ICSharpCode.AvalonEdit/Folding/FoldingManager.cs
http://github.com/icsharpcode/ILSpy · C# · 397 lines · 257 code · 33 blank · 107 comment · 55 complexity · 57f7e4313ff8ad18b0b5e71aa5e717c8 MD5 · raw file
- // 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
- {
- /// <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;
- bool isFirstUpdate = true;
-
- #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);
- }
- #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
- 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
- /// <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
- }
- }