PageRenderTime 89ms CodeModel.GetById 72ms app.highlight 10ms RepoModel.GetById 1ms app.codeStats 1ms

/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
  1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
  2// 
  3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4// software and associated documentation files (the "Software"), to deal in the Software
  5// without restriction, including without limitation the rights to use, copy, modify, merge,
  6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7// to whom the Software is furnished to do so, subject to the following conditions:
  8// 
  9// The above copyright notice and this permission notice shall be included in all copies or
 10// substantial portions of the Software.
 11// 
 12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 17// DEALINGS IN THE SOFTWARE.
 18
 19using System;
 20using System.Collections.Generic;
 21using System.Collections.ObjectModel;
 22using System.Linq;
 23using System.Windows;
 24
 25using ICSharpCode.AvalonEdit.Document;
 26using ICSharpCode.AvalonEdit.Editing;
 27using ICSharpCode.AvalonEdit.Rendering;
 28using ICSharpCode.AvalonEdit.Utils;
 29
 30namespace ICSharpCode.AvalonEdit.Folding
 31{
 32	/// <summary>
 33	/// Stores a list of foldings for a specific TextView and TextDocument.
 34	/// </summary>
 35	public class FoldingManager : IWeakEventListener
 36	{
 37		internal readonly TextDocument document;
 38		
 39		internal readonly List<TextView> textViews = new List<TextView>();
 40		readonly TextSegmentCollection<FoldingSection> foldings;
 41		bool isFirstUpdate = true;
 42		
 43		#region Constructor
 44		/// <summary>
 45		/// Creates a new FoldingManager instance.
 46		/// </summary>
 47		public FoldingManager(TextDocument document)
 48		{
 49			if (document == null)
 50				throw new ArgumentNullException("document");
 51			this.document = document;
 52			this.foldings = new TextSegmentCollection<FoldingSection>();
 53			document.VerifyAccess();
 54			TextDocumentWeakEventManager.Changed.AddListener(document, this);
 55		}
 56		#endregion
 57		
 58		#region ReceiveWeakEvent
 59		/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
 60		protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
 61		{
 62			if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
 63				OnDocumentChanged((DocumentChangeEventArgs)e);
 64				return true;
 65			}
 66			return false;
 67		}
 68		
 69		bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
 70		{
 71			return ReceiveWeakEvent(managerType, sender, e);
 72		}
 73		
 74		void OnDocumentChanged(DocumentChangeEventArgs e)
 75		{
 76			foldings.UpdateOffsets(e);
 77			int newEndOffset = e.Offset + e.InsertionLength;
 78			// extend end offset to the end of the line (including delimiter)
 79			var endLine = document.GetLineByOffset(newEndOffset);
 80			newEndOffset = endLine.Offset + endLine.TotalLength;
 81			foreach (var affectedFolding in foldings.FindOverlappingSegments(e.Offset, newEndOffset - e.Offset)) {
 82				if (affectedFolding.Length == 0) {
 83					RemoveFolding(affectedFolding);
 84				} else {
 85					affectedFolding.ValidateCollapsedLineSections();
 86				}
 87			}
 88		}
 89		#endregion
 90		
 91		#region Manage TextViews
 92		internal void AddToTextView(TextView textView)
 93		{
 94			if (textView == null || textViews.Contains(textView))
 95				throw new ArgumentException();
 96			textViews.Add(textView);
 97			foreach (FoldingSection fs in foldings) {
 98				if (fs.collapsedSections != null) {
 99					Array.Resize(ref fs.collapsedSections, textViews.Count);
100					fs.ValidateCollapsedLineSections();
101				}
102			}
103		}
104		
105		internal void RemoveFromTextView(TextView textView)
106		{
107			int pos = textViews.IndexOf(textView);
108			if (pos < 0)
109				throw new ArgumentException();
110			textViews.RemoveAt(pos);
111			foreach (FoldingSection fs in foldings) {
112				if (fs.collapsedSections != null) {
113					var c = new CollapsedLineSection[textViews.Count];
114					Array.Copy(fs.collapsedSections, 0, c, 0, pos);
115					fs.collapsedSections[pos].Uncollapse();
116					Array.Copy(fs.collapsedSections, pos + 1, c, pos, c.Length - pos);
117					fs.collapsedSections = c;
118				}
119			}
120		}
121		
122		internal void Redraw()
123		{
124			foreach (TextView textView in textViews)
125				textView.Redraw();
126		}
127		
128		internal void Redraw(FoldingSection fs)
129		{
130			foreach (TextView textView in textViews)
131				textView.Redraw(fs);
132		}
133		#endregion
134		
135		#region Create / Remove / Clear
136		/// <summary>
137		/// Creates a folding for the specified text section.
138		/// </summary>
139		public FoldingSection CreateFolding(int startOffset, int endOffset)
140		{
141			if (startOffset >= endOffset)
142				throw new ArgumentException("startOffset must be less than endOffset");
143			if (startOffset < 0 || endOffset > document.TextLength)
144				throw new ArgumentException("Folding must be within document boundary");
145			FoldingSection fs = new FoldingSection(this, startOffset, endOffset);
146			foldings.Add(fs);
147			Redraw(fs);
148			return fs;
149		}
150		
151		/// <summary>
152		/// Removes a folding section from this manager.
153		/// </summary>
154		public void RemoveFolding(FoldingSection fs)
155		{
156			if (fs == null)
157				throw new ArgumentNullException("fs");
158			fs.IsFolded = false;
159			foldings.Remove(fs);
160			Redraw(fs);
161		}
162		
163		/// <summary>
164		/// Removes all folding sections.
165		/// </summary>
166		public void Clear()
167		{
168			document.VerifyAccess();
169			foreach (FoldingSection s in foldings)
170				s.IsFolded = false;
171			foldings.Clear();
172			Redraw();
173		}
174		#endregion
175		
176		#region Get...Folding
177		/// <summary>
178		/// Gets all foldings in this manager.
179		/// The foldings are returned sorted by start offset;
180		/// for multiple foldings at the same offset the order is undefined.
181		/// </summary>
182		public IEnumerable<FoldingSection> AllFoldings {
183			get { return foldings; }
184		}
185		
186		/// <summary>
187		/// Gets the first offset greater or equal to <paramref name="startOffset"/> where a folded folding starts.
188		/// Returns -1 if there are no foldings after <paramref name="startOffset"/>.
189		/// </summary>
190		public int GetNextFoldedFoldingStart(int startOffset)
191		{
192			FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
193			while (fs != null && !fs.IsFolded)
194				fs = foldings.GetNextSegment(fs);
195			return fs != null ? fs.StartOffset : -1;
196		}
197		
198		/// <summary>
199		/// Gets the first folding with a <see cref="TextSegment.StartOffset"/> greater or equal to
200		/// <paramref name="startOffset"/>.
201		/// Returns null if there are no foldings after <paramref name="startOffset"/>.
202		/// </summary>
203		public FoldingSection GetNextFolding(int startOffset)
204		{
205			// TODO: returns the longest folding instead of any folding at the first position after startOffset
206			return foldings.FindFirstSegmentWithStartAfter(startOffset);
207		}
208		
209		/// <summary>
210		/// Gets all foldings that start exactly at <paramref name="startOffset"/>.
211		/// </summary>
212		public ReadOnlyCollection<FoldingSection> GetFoldingsAt(int startOffset)
213		{
214			List<FoldingSection> result = new List<FoldingSection>();
215			FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
216			while (fs != null && fs.StartOffset == startOffset) {
217				result.Add(fs);
218				fs = foldings.GetNextSegment(fs);
219			}
220			return result.AsReadOnly();
221		}
222		
223		/// <summary>
224		/// Gets all foldings that contain <paramref name="offset" />.
225		/// </summary>
226		public ReadOnlyCollection<FoldingSection> GetFoldingsContaining(int offset)
227		{
228			return foldings.FindSegmentsContaining(offset);
229		}
230		#endregion
231		
232		#region UpdateFoldings
233		/// <summary>
234		/// Updates the foldings in this <see cref="FoldingManager"/> using the given new foldings.
235		/// This method will try to detect which new foldings correspond to which existing foldings; and will keep the state
236		/// (<see cref="FoldingSection.IsFolded"/>) for existing foldings.
237		/// </summary>
238		/// <param name="newFoldings">The new set of foldings. These must be sorted by starting offset.</param>
239		/// <param name="firstErrorOffset">The first position of a parse error. Existing foldings starting after
240		/// this offset will be kept even if they don't appear in <paramref name="newFoldings"/>.
241		/// Use -1 for this parameter if there were no parse errors.</param>
242		public void UpdateFoldings(IEnumerable<NewFolding> newFoldings, int firstErrorOffset)
243		{
244			if (newFoldings == null)
245				throw new ArgumentNullException("newFoldings");
246			
247			if (firstErrorOffset < 0)
248				firstErrorOffset = int.MaxValue;
249			
250			var oldFoldings = this.AllFoldings.ToArray();
251			int oldFoldingIndex = 0;
252			int previousStartOffset = 0;
253			// merge new foldings into old foldings so that sections keep being collapsed
254			// both oldFoldings and newFoldings are sorted by start offset
255			foreach (NewFolding newFolding in newFoldings) {
256				// ensure newFoldings are sorted correctly
257				if (newFolding.StartOffset < previousStartOffset)
258					throw new ArgumentException("newFoldings must be sorted by start offset");
259				previousStartOffset = newFolding.StartOffset;
260				
261				int startOffset = newFolding.StartOffset.CoerceValue(0, document.TextLength);
262				int endOffset = newFolding.EndOffset.CoerceValue(0, document.TextLength);
263				
264				if (newFolding.StartOffset == newFolding.EndOffset)
265					continue; // ignore zero-length foldings
266				
267				// remove old foldings that were skipped
268				while (oldFoldingIndex < oldFoldings.Length && newFolding.StartOffset > oldFoldings[oldFoldingIndex].StartOffset) {
269					this.RemoveFolding(oldFoldings[oldFoldingIndex++]);
270				}
271				FoldingSection section;
272				// reuse current folding if its matching:
273				if (oldFoldingIndex < oldFoldings.Length && newFolding.StartOffset == oldFoldings[oldFoldingIndex].StartOffset) {
274					section = oldFoldings[oldFoldingIndex++];
275					section.Length = newFolding.EndOffset - newFolding.StartOffset;
276				} else {
277					// no matching current folding; create a new one:
278					section = this.CreateFolding(newFolding.StartOffset, newFolding.EndOffset);
279					// auto-close #regions only when opening the document
280					if (isFirstUpdate) {
281						section.IsFolded = newFolding.DefaultClosed;
282						isFirstUpdate = false;
283					}
284					section.Tag = newFolding;
285				}
286				section.Title = newFolding.Name;
287			}
288			// remove all outstanding old foldings:
289			while (oldFoldingIndex < oldFoldings.Length) {
290				FoldingSection oldSection = oldFoldings[oldFoldingIndex++];
291				if (oldSection.StartOffset >= firstErrorOffset)
292					break;
293				this.RemoveFolding(oldSection);
294			}
295		}
296		#endregion
297		
298		#region Install
299		/// <summary>
300		/// Adds Folding support to the specified text area.
301		/// Warning: The folding manager is only valid for the text area's current document. The folding manager
302		/// must be uninstalled before the text area is bound to a different document.
303		/// </summary>
304		/// <returns>The <see cref="FoldingManager"/> that manages the list of foldings inside the text area.</returns>
305		public static FoldingManager Install(TextArea textArea)
306		{
307			if (textArea == null)
308				throw new ArgumentNullException("textArea");
309			return new FoldingManagerInstallation(textArea);
310		}
311		
312		/// <summary>
313		/// Uninstalls the folding manager.
314		/// </summary>
315		/// <exception cref="ArgumentException">The specified manager was not created using <see cref="Install"/>.</exception>
316		public static void Uninstall(FoldingManager manager)
317		{
318			if (manager == null)
319				throw new ArgumentNullException("manager");
320			FoldingManagerInstallation installation = manager as FoldingManagerInstallation;
321			if (installation != null) {
322				installation.Uninstall();
323			} else {
324				throw new ArgumentException("FoldingManager was not created using FoldingManager.Install");
325			}
326		}
327		
328		sealed class FoldingManagerInstallation : FoldingManager
329		{
330			TextArea textArea;
331			FoldingMargin margin;
332			FoldingElementGenerator generator;
333			
334			public FoldingManagerInstallation(TextArea textArea) : base(textArea.Document)
335			{
336				this.textArea = textArea;
337				margin = new FoldingMargin() { FoldingManager = this };
338				generator = new FoldingElementGenerator() { FoldingManager = this };
339				textArea.LeftMargins.Add(margin);
340				textArea.TextView.Services.AddService(typeof(FoldingManager), this);
341				// HACK: folding only works correctly when it has highest priority
342				textArea.TextView.ElementGenerators.Insert(0, generator);
343				textArea.Caret.PositionChanged += textArea_Caret_PositionChanged;
344			}
345			
346			/*
347			void DemoMode()
348			{
349				foldingGenerator = new FoldingElementGenerator() { FoldingManager = fm };
350				foldingMargin = new FoldingMargin { FoldingManager = fm };
351				foldingMarginBorder = new Border {
352					Child = foldingMargin,
353					Background = new LinearGradientBrush(Colors.White, Colors.Transparent, 0)
354				};
355				foldingMarginBorder.SizeChanged += UpdateTextViewClip;
356				textEditor.TextArea.TextView.ElementGenerators.Add(foldingGenerator);
357				textEditor.TextArea.LeftMargins.Add(foldingMarginBorder);
358			}
359			
360			void UpdateTextViewClip(object sender, SizeChangedEventArgs e)
361			{
362				textEditor.TextArea.TextView.Clip = new RectangleGeometry(
363					new Rect(-foldingMarginBorder.ActualWidth,
364					         0,
365					         textEditor.TextArea.TextView.ActualWidth + foldingMarginBorder.ActualWidth,
366					         textEditor.TextArea.TextView.ActualHeight));
367			}
368			 */
369			
370			public void Uninstall()
371			{
372				Clear();
373				if (textArea != null) {
374					textArea.Caret.PositionChanged -= textArea_Caret_PositionChanged;
375					textArea.LeftMargins.Remove(margin);
376					textArea.TextView.ElementGenerators.Remove(generator);
377					textArea.TextView.Services.RemoveService(typeof(FoldingManager));
378					margin = null;
379					generator = null;
380					textArea = null;
381				}
382			}
383			
384			void textArea_Caret_PositionChanged(object sender, EventArgs e)
385			{
386				// Expand Foldings when Caret is moved into them.
387				int caretOffset = textArea.Caret.Offset;
388				foreach (FoldingSection s in GetFoldingsContaining(caretOffset)) {
389					if (s.IsFolded && s.StartOffset < caretOffset && caretOffset < s.EndOffset) {
390						s.IsFolded = false;
391					}
392				}
393			}
394		}
395		#endregion
396	}
397}