// 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 } }