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