diff options
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding')
9 files changed, 1675 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/AbstractFoldingStrategy.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/AbstractFoldingStrategy.cs new file mode 100644 index 000000000..42eddba47 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/AbstractFoldingStrategy.cs @@ -0,0 +1,30 @@ +// 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 Tango.Scripting.Editors.Document; +using System.Collections.Generic; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// Base class for folding strategies. + /// </summary> + public abstract class AbstractFoldingStrategy + { + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document and updates the folding manager with them. + /// </summary> + public void UpdateFoldings(FoldingManager manager, TextDocument document) + { + int firstErrorOffset; + IEnumerable<NewFolding> foldings = CreateNewFoldings(document, out firstErrorOffset); + manager.UpdateFoldings(foldings, firstErrorOffset); + } + + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document. + /// </summary> + public abstract IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset); + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/BraceFoldingStrategy.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/BraceFoldingStrategy.cs new file mode 100644 index 000000000..0b26184ee --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/BraceFoldingStrategy.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// Allows producing foldings from a document based on braces. + /// </summary> + public class BraceFoldingStrategy + { + private class FoldingDefinition + { + public String Open { get; set; } + public String Close { get; set; } + + public FoldingDefinition(String open, String close) + { + Open = open; + Close = close; + } + } + + private class FoldingOffset + { + public FoldingDefinition Definition { get; set; } + public int Offset { get; set; } + } + + private List<FoldingDefinition> foldings = new List<FoldingDefinition>() + { + new FoldingDefinition("namespace","}"), + new FoldingDefinition("public","}"), + new FoldingDefinition("private","}"), + new FoldingDefinition("internal","}"), + new FoldingDefinition("if","}"), + new FoldingDefinition("class","}"), + new FoldingDefinition("while","}"), + new FoldingDefinition("static void","}"), + new FoldingDefinition("/// <summary>","/// </summary>"), + }; + + /// <summary> + /// Gets/Sets the opening brace. The default value is '{'. + /// </summary> + public char OpeningBrace { get; set; } + + /// <summary> + /// Gets/Sets the closing brace. The default value is '}'. + /// </summary> + public char ClosingBrace { get; set; } + + /// <summary> + /// Creates a new BraceFoldingStrategy. + /// </summary> + public BraceFoldingStrategy() + { + this.OpeningBrace = '{'; + this.ClosingBrace = '}'; + } + + public void UpdateFoldings(FoldingManager manager, TextDocument document) + { + int firstErrorOffset; + IEnumerable<NewFolding> newFoldings = CreateNewFoldings(document, out firstErrorOffset); + manager.UpdateFoldings(newFoldings, firstErrorOffset); + } + + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document. + /// </summary> + public IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset) + { + firstErrorOffset = -1; + return CreateNewFoldings(document); + } + + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document. + /// </summary> + public IEnumerable<NewFolding> CreateNewFoldings(ITextSource document) + { + List<NewFolding> newFoldings = new List<NewFolding>(); + + Stack<FoldingOffset> startOffsets = new Stack<FoldingOffset>(); + int lastNewLineOffset = 0; + char openingBrace = this.OpeningBrace; + char closingBrace = this.ClosingBrace; + + var doc = document as TextDocument; + + String current_line = String.Empty; + + foreach (var line in doc.Lines) + { + String lineTextFull = doc.GetText(line); + String lineText = lineTextFull.TrimStart('\t', ' ').TrimEnd('\t', ' '); + + var open_definition = foldings.SingleOrDefault(x => lineText.StartsWith(x.Open + " ") || lineText == x.Open); + + if (open_definition != null) + { + current_line = lineTextFull; + startOffsets.Push(new FoldingOffset() + { + Definition = open_definition, + Offset = line.EndOffset + }); + } + else if (foldings.Any(x => lineText.EndsWith(x.Close))) + { + if (startOffsets.Count > 0) + { + var startOffset = startOffsets.Pop(); + //if (startOffset < lastNewLineOffset) + //{ + newFoldings.Add(new NewFolding(startOffset.Offset, Math.Min(line.EndOffset, doc.TextLength)) + { + //Name = current_line, + }); + } + //} + } + else if (lineText.EndsWith("\n") || lineText.EndsWith("\r")) + { + lastNewLineOffset = line.Offset + 1; + } + } + + //for (int i = 0; i < document.TextLength; i++) + //{ + // char c = document.GetCharAt(i); + // if (c == openingBrace) + // { + // startOffsets.Push(i); + // } + // else if (c == closingBrace && startOffsets.Count > 0) + // { + // int startOffset = startOffsets.Pop(); + // // don't fold if opening and closing brace are on the same line + // if (startOffset < lastNewLineOffset) + // { + // newFoldings.Add(new NewFolding(startOffset, i + 1)); + // } + // } + // else if (c == '\n' || c == '\r') + // { + // lastNewLineOffset = i + 1; + // } + //} + + newFoldings.Sort((a, b) => a.StartOffset.CompareTo(b.StartOffset)); + return newFoldings; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingElementGenerator.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingElementGenerator.cs new file mode 100644 index 000000000..76a1da101 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingElementGenerator.cs @@ -0,0 +1,194 @@ +// 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.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// A <see cref="VisualLineElementGenerator"/> that produces line elements for folded <see cref="FoldingSection"/>s. + /// </summary> + public sealed class FoldingElementGenerator : VisualLineElementGenerator, ITextViewConnect + { + readonly List<TextView> textViews = new List<TextView>(); + FoldingManager foldingManager; + + #region FoldingManager property / connecting with TextView + /// <summary> + /// Gets/Sets the folding manager from which the foldings should be shown. + /// </summary> + public FoldingManager FoldingManager { + get { + return foldingManager; + } + set { + if (foldingManager != value) { + if (foldingManager != null) { + foreach (TextView v in textViews) + foldingManager.RemoveFromTextView(v); + } + foldingManager = value; + if (foldingManager != null) { + foreach (TextView v in textViews) + foldingManager.AddToTextView(v); + } + } + } + } + + void ITextViewConnect.AddToTextView(TextView textView) + { + textViews.Add(textView); + if (foldingManager != null) + foldingManager.AddToTextView(textView); + } + + void ITextViewConnect.RemoveFromTextView(TextView textView) + { + textViews.Remove(textView); + if (foldingManager != null) + foldingManager.RemoveFromTextView(textView); + } + #endregion + + /// <inheritdoc/> + public override void StartGeneration(ITextRunConstructionContext context) + { + base.StartGeneration(context); + if (foldingManager != null) { + if (!foldingManager.textViews.Contains(context.TextView)) + throw new ArgumentException("Invalid TextView"); + if (context.Document != foldingManager.document) + throw new ArgumentException("Invalid document"); + } + } + + /// <inheritdoc/> + public override int GetFirstInterestedOffset(int startOffset) + { + if (foldingManager != null) { + foreach (FoldingSection fs in foldingManager.GetFoldingsContaining(startOffset)) { + // Test whether we're currently within a folded folding (that didn't just end). + // If so, create the fold marker immediately. + // This is necessary if the actual beginning of the fold marker got skipped due to another VisualElementGenerator. + if (fs.IsFolded && fs.EndOffset > startOffset) { + //return startOffset; + } + } + return foldingManager.GetNextFoldedFoldingStart(startOffset); + } else { + return -1; + } + } + + /// <inheritdoc/> + public override VisualLineElement ConstructElement(int offset) + { + if (foldingManager == null) + return null; + int foldedUntil = -1; + FoldingSection foldingSection = null; + foreach (FoldingSection fs in foldingManager.GetFoldingsContaining(offset)) { + if (fs.IsFolded) { + if (fs.EndOffset > foldedUntil) { + foldedUntil = fs.EndOffset; + foldingSection = fs; + } + } + } + if (foldedUntil > offset && foldingSection != null) { + // Handle overlapping foldings: if there's another folded folding + // (starting within the foldingSection) that continues after the end of the folded section, + // then we'll extend our fold element to cover that overlapping folding. + bool foundOverlappingFolding; + do { + foundOverlappingFolding = false; + foreach (FoldingSection fs in FoldingManager.GetFoldingsContaining(foldedUntil)) { + if (fs.IsFolded && fs.EndOffset > foldedUntil) { + foldedUntil = fs.EndOffset; + foundOverlappingFolding = true; + } + } + } while (foundOverlappingFolding); + + string title = foldingSection.Title; + if (string.IsNullOrEmpty(title)) + title = "..."; + var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); + p.SetForegroundBrush(textBrush); + var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); + var text = FormattedTextElement.PrepareText(textFormatter, title, p); + return new FoldingLineElement(foldingSection, text, foldedUntil - offset) { textBrush = textBrush }; + } else { + return null; + } + } + + sealed class FoldingLineElement : FormattedTextElement + { + readonly FoldingSection fs; + + internal Brush textBrush; + + public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength) : base(text, documentLength) + { + this.fs = fs; + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + return new FoldingLineTextRun(this, this.TextRunProperties) { textBrush = textBrush }; + } + + protected internal override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) { + fs.IsFolded = false; + e.Handled = true; + } else { + base.OnMouseDown(e); + } + } + } + + sealed class FoldingLineTextRun : FormattedTextRun + { + internal Brush textBrush; + + public FoldingLineTextRun(FormattedTextElement element, TextRunProperties properties) + : base(element, properties) + { + } + + public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways) + { + var metrics = Format(double.PositiveInfinity); + Rect r = new Rect(origin.X, origin.Y - metrics.Baseline, metrics.Width, metrics.Height); + drawingContext.DrawRectangle(null, new Pen(textBrush, 1), r); + base.Draw(drawingContext, origin, rightToLeft, sideways); + } + } + + /// <summary> + /// Default brush for folding element text. Value: Brushes.Gray + /// </summary> + public static readonly Brush DefaultTextBrush = Brushes.Gray; + + static Brush textBrush = DefaultTextBrush; + + /// <summary> + /// Gets/sets the brush used for folding element text. + /// </summary> + public static Brush TextBrush { + get { return textBrush; } + set { textBrush = value; } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingManager.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingManager.cs new file mode 100644 index 000000000..4035928b4 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingManager.cs @@ -0,0 +1,388 @@ +// 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 Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.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.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 + 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 + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMargin.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMargin.cs new file mode 100644 index 000000000..c3d03646c --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMargin.cs @@ -0,0 +1,343 @@ +// 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.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// A margin that shows markers for foldings and allows to expand/collapse the foldings. + /// </summary> + public class FoldingMargin : AbstractMargin + { + /// <summary> + /// Gets/Sets the folding manager from which the foldings should be shown. + /// </summary> + public FoldingManager FoldingManager { get; set; } + + internal const double SizeFactor = Constants.PixelPerPoint; + + #region Brushes + /// <summary> + /// FoldingMarkerBrush dependency property. + /// </summary> + public static readonly DependencyProperty FoldingMarkerBrushProperty = + DependencyProperty.RegisterAttached("FoldingMarkerBrush", typeof(Brush), typeof(FoldingMargin), + new FrameworkPropertyMetadata(Brushes.Gainsboro)); + + /// <summary> + /// Gets/sets the Brush used for displaying the lines of folding markers. + /// </summary> + public Brush FoldingMarkerBrush { + get { return (Brush)GetValue(FoldingMarkerBrushProperty); } + set { SetValue(FoldingMarkerBrushProperty, value); } + } + + /// <summary> + /// FoldingMarkerBackgroundBrush dependency property. + /// </summary> + public static readonly DependencyProperty FoldingMarkerBackgroundBrushProperty = + DependencyProperty.RegisterAttached("FoldingMarkerBackgroundBrush", typeof(Brush), typeof(FoldingMargin), + new FrameworkPropertyMetadata(Brushes.Transparent)); + + /// <summary> + /// Gets/sets the Brush used for displaying the background of folding markers. + /// </summary> + public Brush FoldingMarkerBackgroundBrush { + get { return (Brush)GetValue(FoldingMarkerBackgroundBrushProperty); } + set { SetValue(FoldingMarkerBackgroundBrushProperty, value); } + } + + /// <summary> + /// SelectedFoldingMarkerBrush dependency property. + /// </summary> + public static readonly DependencyProperty SelectedFoldingMarkerBrushProperty = + DependencyProperty.RegisterAttached("SelectedFoldingMarkerBrush", + typeof(Brush), typeof(FoldingMargin), + new FrameworkPropertyMetadata(Brushes.Gainsboro)); + + /// <summary> + /// Gets/sets the Brush used for displaying the lines of selected folding markers. + /// </summary> + public Brush SelectedFoldingMarkerBrush { + get { return (Brush)GetValue(SelectedFoldingMarkerBrushProperty); } + set { SetValue(SelectedFoldingMarkerBrushProperty, value); } + } + + /// <summary> + /// SelectedFoldingMarkerBackgroundBrush dependency property. + /// </summary> + public static readonly DependencyProperty SelectedFoldingMarkerBackgroundBrushProperty = + DependencyProperty.RegisterAttached("SelectedFoldingMarkerBackgroundBrush", + typeof(Brush), typeof(FoldingMargin), + new FrameworkPropertyMetadata(Brushes.Transparent)); + + /// <summary> + /// Gets/sets the Brush used for displaying the background of selected folding markers. + /// </summary> + public Brush SelectedFoldingMarkerBackgroundBrush { + get { return (Brush)GetValue(SelectedFoldingMarkerBackgroundBrushProperty); } + set { SetValue(SelectedFoldingMarkerBackgroundBrushProperty, value); } + } + + static void OnUpdateBrushes(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + FoldingMargin m = null; + if (d is FoldingMargin) + m = (FoldingMargin)d; + else if (d is TextEditor) + m = ((TextEditor)d).TextArea.LeftMargins.FirstOrDefault(c => c is FoldingMargin) as FoldingMargin; + if (m == null) return; + if (e.Property.Name == FoldingMarkerBrushProperty.Name) + m.foldingControlPen = MakeFrozenPen((Brush)e.NewValue); + if (e.Property.Name == SelectedFoldingMarkerBrushProperty.Name) + m.selectedFoldingControlPen = MakeFrozenPen((Brush)e.NewValue); + } + #endregion + + /// <inheritdoc/> + protected override Size MeasureOverride(Size availableSize) + { + foreach (FoldingMarginMarker m in markers) { + m.Measure(availableSize); + } + double width = SizeFactor * (double)GetValue(TextBlock.FontSizeProperty); + return new Size(PixelSnapHelpers.RoundToOdd(width, PixelSnapHelpers.GetPixelSize(this).Width), 0); + } + + /// <inheritdoc/> + protected override Size ArrangeOverride(Size finalSize) + { + Size pixelSize = PixelSnapHelpers.GetPixelSize(this); + foreach (FoldingMarginMarker m in markers) { + int visualColumn = m.VisualLine.GetVisualColumn(m.FoldingSection.StartOffset - m.VisualLine.FirstDocumentLine.Offset); + TextLine textLine = m.VisualLine.GetTextLine(visualColumn); + double yPos = m.VisualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextMiddle) - TextView.VerticalOffset; + yPos -= m.DesiredSize.Height / 2; + double xPos = (finalSize.Width - m.DesiredSize.Width) / 2; + m.Arrange(new Rect(PixelSnapHelpers.Round(new Point(xPos, yPos), pixelSize), m.DesiredSize)); + } + return base.ArrangeOverride(finalSize); + } + + /// <inheritdoc/> + protected override void OnTextViewChanged(TextView oldTextView, TextView newTextView) + { + if (oldTextView != null) { + oldTextView.VisualLinesChanged -= TextViewVisualLinesChanged; + } + base.OnTextViewChanged(oldTextView, newTextView); + if (newTextView != null) { + newTextView.VisualLinesChanged += TextViewVisualLinesChanged; + } + TextViewVisualLinesChanged(null, null); + } + + List<FoldingMarginMarker> markers = new List<FoldingMarginMarker>(); + + void TextViewVisualLinesChanged(object sender, EventArgs e) + { + foreach (FoldingMarginMarker m in markers) { + RemoveVisualChild(m); + } + markers.Clear(); + InvalidateVisual(); + if (TextView != null && FoldingManager != null && TextView.VisualLinesValid) { + foreach (VisualLine line in TextView.VisualLines) { + FoldingSection fs = FoldingManager.GetNextFolding(line.FirstDocumentLine.Offset); + if (fs == null) + continue; + if (fs.StartOffset <= line.LastDocumentLine.Offset + line.LastDocumentLine.Length) { + FoldingMarginMarker m = new FoldingMarginMarker { + IsExpanded = !fs.IsFolded, + VisualLine = line, + FoldingSection = fs + }; + + markers.Add(m); + AddVisualChild(m); + + m.IsMouseDirectlyOverChanged += delegate { InvalidateVisual(); }; + + InvalidateMeasure(); + continue; + } + } + } + } + + /// <inheritdoc/> + protected override int VisualChildrenCount { + get { return markers.Count; } + } + + /// <inheritdoc/> + protected override Visual GetVisualChild(int index) + { + return markers[index]; + } + + Pen foldingControlPen = MakeFrozenPen((Brush)FoldingMarkerBrushProperty.DefaultMetadata.DefaultValue); + Pen selectedFoldingControlPen = MakeFrozenPen((Brush)SelectedFoldingMarkerBrushProperty.DefaultMetadata.DefaultValue); + + static Pen MakeFrozenPen(Brush brush) + { + Pen pen = new Pen(brush, 1); + pen.Freeze(); + return pen; + } + + /// <inheritdoc/> + protected override void OnRender(DrawingContext drawingContext) + { + if (TextView == null || !TextView.VisualLinesValid) + return; + if (TextView.VisualLines.Count == 0 || FoldingManager == null) + return; + + var allTextLines = TextView.VisualLines.SelectMany(vl => vl.TextLines).ToList(); + Pen[] colors = new Pen[allTextLines.Count + 1]; + Pen[] endMarker = new Pen[allTextLines.Count]; + + CalculateFoldLinesForFoldingsActiveAtStart(allTextLines, colors, endMarker); + CalculateFoldLinesForMarkers(allTextLines, colors, endMarker); + DrawFoldLines(drawingContext, colors, endMarker); + + base.OnRender(drawingContext); + } + + /// <summary> + /// Calculates fold lines for all folding sections that start in front of the current view + /// and run into the current view. + /// </summary> + void CalculateFoldLinesForFoldingsActiveAtStart(List<TextLine> allTextLines, Pen[] colors, Pen[] endMarker) + { + int viewStartOffset = TextView.VisualLines[0].FirstDocumentLine.Offset; + int viewEndOffset = TextView.VisualLines.Last().LastDocumentLine.EndOffset; + var foldings = FoldingManager.GetFoldingsContaining(viewStartOffset); + int maxEndOffset = 0; + foreach (FoldingSection fs in foldings) { + int end = fs.EndOffset; + if (end <= viewEndOffset && !fs.IsFolded) { + int textLineNr = GetTextLineIndexFromOffset(allTextLines, end); + if (textLineNr >= 0) { + endMarker[textLineNr] = foldingControlPen; + } + } + if (end > maxEndOffset && fs.StartOffset < viewStartOffset) { + maxEndOffset = end; + } + } + if (maxEndOffset > 0) { + if (maxEndOffset > viewEndOffset) { + for (int i = 0; i < colors.Length; i++) { + colors[i] = foldingControlPen; + } + } else { + int maxTextLine = GetTextLineIndexFromOffset(allTextLines, maxEndOffset); + for (int i = 0; i <= maxTextLine; i++) { + colors[i] = foldingControlPen; + } + } + } + } + + /// <summary> + /// Calculates fold lines for all folding sections that start inside the current view + /// </summary> + void CalculateFoldLinesForMarkers(List<TextLine> allTextLines, Pen[] colors, Pen[] endMarker) + { + foreach (FoldingMarginMarker marker in markers) { + int end = marker.FoldingSection.EndOffset; + int endTextLineNr = GetTextLineIndexFromOffset(allTextLines, end); + if (!marker.FoldingSection.IsFolded && endTextLineNr >= 0) { + if (marker.IsMouseDirectlyOver) + endMarker[endTextLineNr] = selectedFoldingControlPen; + else if (endMarker[endTextLineNr] == null) + endMarker[endTextLineNr] = foldingControlPen; + } + int startTextLineNr = GetTextLineIndexFromOffset(allTextLines, marker.FoldingSection.StartOffset); + if (startTextLineNr >= 0) { + for (int i = startTextLineNr + 1; i < colors.Length && i - 1 != endTextLineNr; i++) { + if (marker.IsMouseDirectlyOver) + colors[i] = selectedFoldingControlPen; + else if (colors[i] == null) + colors[i] = foldingControlPen; + } + } + } + } + + /// <summary> + /// Draws the lines for the folding sections (vertical line with 'color', horizontal lines with 'endMarker') + /// Each entry in the input arrays corresponds to one TextLine. + /// </summary> + void DrawFoldLines(DrawingContext drawingContext, Pen[] colors, Pen[] endMarker) + { + // Because we are using PenLineCap.Flat (the default), for vertical lines, + // Y coordinates must be on pixel boundaries, whereas the X coordinate must be in the + // middle of a pixel. (and the other way round for horizontal lines) + Size pixelSize = PixelSnapHelpers.GetPixelSize(this); + double markerXPos = PixelSnapHelpers.PixelAlign(RenderSize.Width / 2, pixelSize.Width); + double startY = 0; + Pen currentPen = colors[0]; + + Pen p = new Pen(FoldingMarkerBrush, 1); + + int tlNumber = 0; + foreach (VisualLine vl in TextView.VisualLines) { + foreach (TextLine tl in vl.TextLines) { + if (endMarker[tlNumber] != null) { + double visualPos = GetVisualPos(vl, tl, pixelSize.Height); + drawingContext.DrawLine(p, new Point(markerXPos - pixelSize.Width / 2, visualPos), new Point(RenderSize.Width - 4, visualPos)); + } + if (colors[tlNumber + 1] != currentPen) { + double visualPos = GetVisualPos(vl, tl, pixelSize.Height); + if (currentPen != null) { + drawingContext.DrawLine(p, new Point(markerXPos, startY + pixelSize.Height), new Point(markerXPos, visualPos - pixelSize.Height / 2)); + } + currentPen = colors[tlNumber + 1]; + startY = visualPos; + } + tlNumber++; + } + } + if (currentPen != null) { + drawingContext.DrawLine(currentPen, new Point(markerXPos, startY + pixelSize.Height / 2), new Point(markerXPos, RenderSize.Height)); + } + } + + double GetVisualPos(VisualLine vl, TextLine tl, double pixelHeight) + { + double pos = vl.GetTextLineVisualYPosition(tl, VisualYPosition.TextMiddle) - TextView.VerticalOffset; + return PixelSnapHelpers.PixelAlign(pos, pixelHeight); + } + + int GetTextLineIndexFromOffset(List<TextLine> textLines, int offset) + { + int lineNumber = TextView.Document.GetLineByOffset(offset).LineNumber; + VisualLine vl = TextView.GetVisualLine(lineNumber); + if (vl != null) { + int relOffset = offset - vl.FirstDocumentLine.Offset; + TextLine line = vl.GetTextLine(vl.GetVisualColumn(relOffset)); + return textLines.IndexOf(line); + } + return -1; + } + + static FoldingMargin() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(FoldingMargin), new FrameworkPropertyMetadata(typeof(FoldingMargin))); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMarginMarker.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMarginMarker.cs new file mode 100644 index 000000000..5b69d7485 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingMarginMarker.cs @@ -0,0 +1,90 @@ +// 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.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Folding +{ + sealed class FoldingMarginMarker : UIElement + { + internal VisualLine VisualLine; + internal FoldingSection FoldingSection; + + bool isExpanded; + + public bool IsExpanded { + get { return isExpanded; } + set { + if (isExpanded != value) { + isExpanded = value; + InvalidateVisual(); + } + if (FoldingSection != null) + FoldingSection.IsFolded = !value; + } + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + if (!e.Handled) { + if (e.ChangedButton == MouseButton.Left) { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } + } + + const double MarginSizeFactor = 0.7; + + protected override Size MeasureCore(Size availableSize) + { + double size = MarginSizeFactor * FoldingMargin.SizeFactor * (double)GetValue(TextBlock.FontSizeProperty); + size = PixelSnapHelpers.RoundToOdd(size, PixelSnapHelpers.GetPixelSize(this).Width); + return new Size(size, size); + } + + protected override void OnRender(DrawingContext drawingContext) + { + FoldingMargin margin = VisualParent as FoldingMargin; + Pen activePen = new Pen(margin.SelectedFoldingMarkerBrush, 1); + + Pen inactivePen = new Pen(margin.FoldingMarkerBrush, 1); + activePen.StartLineCap = inactivePen.StartLineCap = PenLineCap.Square; + activePen.EndLineCap = inactivePen.EndLineCap = PenLineCap.Square; + Size pixelSize = PixelSnapHelpers.GetPixelSize(this); + Rect rect = new Rect(pixelSize.Width / 2, + pixelSize.Height / 2, + this.RenderSize.Width - pixelSize.Width - 2, + this.RenderSize.Height - pixelSize.Height - 2); + + drawingContext.DrawRectangle( + IsMouseDirectlyOver ? margin.FoldingMarkerBackgroundBrush : margin.FoldingMarkerBackgroundBrush, + IsMouseDirectlyOver ? activePen : inactivePen, rect); + double middleX = rect.Left + rect.Width / 2; + double middleY = rect.Top + rect.Height / 2; + double space = PixelSnapHelpers.Round(rect.Width / 8, pixelSize.Width) + pixelSize.Width; + drawingContext.DrawLine(activePen, + new Point(rect.Left + space, middleY), + new Point(rect.Right - space, middleY)); + if (!isExpanded) { + drawingContext.DrawLine(activePen, + new Point(middleX, rect.Top + space), + new Point(middleX, rect.Bottom - space)); + } + } + + protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e) + { + base.OnIsMouseDirectlyOverChanged(e); + InvalidateVisual(); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingSection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingSection.cs new file mode 100644 index 000000000..fd27e9b3b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/FoldingSection.cs @@ -0,0 +1,186 @@ +// 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.Diagnostics; +using System.Text; +using System.Windows.Threading; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// A section that can be folded. + /// </summary> + public sealed class FoldingSection : TextSegment + { + readonly FoldingManager manager; + bool isFolded; + internal CollapsedLineSection[] collapsedSections; + string title; + + /// <summary> + /// Gets/sets if the section is folded. + /// </summary> + public bool IsFolded { + get { return isFolded; } + set { + if (isFolded != value) { + isFolded = value; + ValidateCollapsedLineSections(); // create/destroy CollapsedLineSection + manager.Redraw(this); + } + } + } + + internal void ValidateCollapsedLineSections() + { + if (!isFolded) { + RemoveCollapsedLineSection(); + return; + } + // It is possible that StartOffset/EndOffset get set to invalid values via the property setters in TextSegment, + // so we coerce those values into the valid range. + DocumentLine startLine = manager.document.GetLineByOffset(StartOffset.CoerceValue(0, manager.document.TextLength)); + DocumentLine endLine = manager.document.GetLineByOffset(EndOffset.CoerceValue(0, manager.document.TextLength)); + if (startLine == endLine) { + RemoveCollapsedLineSection(); + } else { + if (collapsedSections == null) + collapsedSections = new CollapsedLineSection[manager.textViews.Count]; + // Validate collapsed line sections + DocumentLine startLinePlusOne = startLine.NextLine; + for (int i = 0; i < collapsedSections.Length; i++) { + var collapsedSection = collapsedSections[i]; + if (collapsedSection == null || collapsedSection.Start != startLinePlusOne || collapsedSection.End != endLine) { + // recreate this collapsed section + if (collapsedSection != null) { + Debug.WriteLine("CollapsedLineSection validation - recreate collapsed section from " + startLinePlusOne + " to " + endLine); + collapsedSection.Uncollapse(); + } + collapsedSections[i] = manager.textViews[i].CollapseLines(startLinePlusOne, endLine); + } + } + } + } + + /// <inheritdoc/> + protected override void OnSegmentChanged() + { + ValidateCollapsedLineSections(); + base.OnSegmentChanged(); + // don't redraw if the FoldingSection wasn't added to the FoldingManager's collection yet + if (IsConnectedToCollection) + manager.Redraw(this); + } + + /// <summary> + /// Gets/Sets the text used to display the collapsed version of the folding section. + /// </summary> + public string Title { + get { + return title; + } + set { + if (title != value) { + title = value; + if (this.IsFolded) + manager.Redraw(this); + } + } + } + + /// <summary> + /// Gets the content of the collapsed lines as text. + /// </summary> + public string TextContent { + get { + return manager.document.GetText(StartOffset, EndOffset - StartOffset); + } + } + + /// <summary> + /// Gets the content of the collapsed lines as tooltip text. + /// </summary> + [Obsolete] + public string TooltipText { + get { + // This fixes SD-1394: + // Each line is checked for leading indentation whitespaces. If + // a line has the same or more indentation than the first line, + // it is reduced. If a line is less indented than the first line + // the indentation is removed completely. + // + // See the following example: + // line 1 + // line 2 + // line 3 + // line 4 + // + // is reduced to: + // line 1 + // line 2 + // line 3 + // line 4 + + var startLine = manager.document.GetLineByOffset(StartOffset); + var endLine = manager.document.GetLineByOffset(EndOffset); + var builder = new StringBuilder(); + + var current = startLine; + ISegment startIndent = TextUtilities.GetLeadingWhitespace(manager.document, startLine); + + while (current != endLine.NextLine) { + ISegment currentIndent = TextUtilities.GetLeadingWhitespace(manager.document, current); + + if (current == startLine && current == endLine) + builder.Append(manager.document.GetText(StartOffset, EndOffset - StartOffset)); + else if (current == startLine) { + if (current.EndOffset - StartOffset > 0) + builder.AppendLine(manager.document.GetText(StartOffset, current.EndOffset - StartOffset).TrimStart()); + } else if (current == endLine) { + if (startIndent.Length <= currentIndent.Length) + builder.Append(manager.document.GetText(current.Offset + startIndent.Length, EndOffset - current.Offset - startIndent.Length)); + else + builder.Append(manager.document.GetText(current.Offset + currentIndent.Length, EndOffset - current.Offset - currentIndent.Length)); + } else { + if (startIndent.Length <= currentIndent.Length) + builder.AppendLine(manager.document.GetText(current.Offset + startIndent.Length, current.Length - startIndent.Length)); + else + builder.AppendLine(manager.document.GetText(current.Offset + currentIndent.Length, current.Length - currentIndent.Length)); + } + + current = current.NextLine; + } + + return builder.ToString(); + } + } + + /// <summary> + /// Gets/Sets an additional object associated with this folding section. + /// </summary> + public object Tag { get; set; } + + internal FoldingSection(FoldingManager manager, int startOffset, int endOffset) + { + Debug.Assert(manager != null); + this.manager = manager; + this.StartOffset = startOffset; + this.Length = endOffset - startOffset; + } + + void RemoveCollapsedLineSection() + { + if (collapsedSections != null) { + foreach (var collapsedSection in collapsedSections) { + if (collapsedSection != null && collapsedSection.Start != null) + collapsedSection.Uncollapse(); + } + collapsedSections = null; + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/NewFolding.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/NewFolding.cs new file mode 100644 index 000000000..7f397c82e --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/NewFolding.cs @@ -0,0 +1,70 @@ +// 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.Windows; +using System.Windows.Threading; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// Helper class used for <see cref="FoldingManager.UpdateFoldings"/>. + /// </summary> + public class NewFolding : ISegment + { + /// <summary> + /// Gets/Sets the start offset. + /// </summary> + public int StartOffset { get; set; } + + /// <summary> + /// Gets/Sets the end offset. + /// </summary> + public int EndOffset { get; set; } + + /// <summary> + /// Gets/Sets the name displayed for the folding. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets/Sets whether the folding is closed by default. + /// </summary> + public bool DefaultClosed { get; set; } + + /// <summary> + /// Creates a new NewFolding instance. + /// </summary> + public NewFolding() + { + } + + /// <summary> + /// Creates a new NewFolding instance. + /// </summary> + public NewFolding(int start, int end) + { + if (!(start <= end)) + throw new ArgumentException("'start' must be less than 'end'"); + this.StartOffset = start; + this.EndOffset = end; + this.Name = null; + this.DefaultClosed = false; + } + + int ISegment.Offset { + get { return this.StartOffset; } + } + + int ISegment.Length { + get { return this.EndOffset - this.StartOffset; } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/XmlFoldingStrategy.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/XmlFoldingStrategy.cs new file mode 100644 index 000000000..0f888a572 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Folding/XmlFoldingStrategy.cs @@ -0,0 +1,215 @@ +// 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.Linq; +using System.Text; +using System.Xml; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Folding +{ + /// <summary> + /// Holds information about the start of a fold in an xml string. + /// </summary> + sealed class XmlFoldStart : NewFolding + { + internal int StartLine; + } + + /// <summary> + /// Determines folds for an xml string in the editor. + /// </summary> + public class XmlFoldingStrategy : AbstractFoldingStrategy + { + /// <summary> + /// Flag indicating whether attributes should be displayed on folded + /// elements. + /// </summary> + public bool ShowAttributesWhenFolded { get; set; } + + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document. + /// </summary> + public override IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset) + { + try { + XmlTextReader reader = new XmlTextReader(document.CreateReader()); + reader.XmlResolver = null; // don't resolve DTDs + return CreateNewFoldings(document, reader, out firstErrorOffset); + } catch (XmlException) { + firstErrorOffset = 0; + return Enumerable.Empty<NewFolding>(); + } + } + + /// <summary> + /// Create <see cref="NewFolding"/>s for the specified document. + /// </summary> + public IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, XmlReader reader, out int firstErrorOffset) + { + Stack<XmlFoldStart> stack = new Stack<XmlFoldStart>(); + List<NewFolding> foldMarkers = new List<NewFolding>(); + try { + while (reader.Read()) { + switch (reader.NodeType) { + case XmlNodeType.Element: + if (!reader.IsEmptyElement) { + XmlFoldStart newFoldStart = CreateElementFoldStart(document, reader); + stack.Push(newFoldStart); + } + break; + + case XmlNodeType.EndElement: + XmlFoldStart foldStart = stack.Pop(); + CreateElementFold(document, foldMarkers, reader, foldStart); + break; + + case XmlNodeType.Comment: + CreateCommentFold(document, foldMarkers, reader); + break; + } + } + firstErrorOffset = -1; + } catch (XmlException ex) { + // ignore errors at invalid positions (prevent ArgumentOutOfRangeException) + if (ex.LineNumber >= 1 && ex.LineNumber <= document.LineCount) + firstErrorOffset = document.GetOffset(ex.LineNumber, ex.LinePosition); + else + firstErrorOffset = 0; + } + foldMarkers.Sort((a,b) => a.StartOffset.CompareTo(b.StartOffset)); + return foldMarkers; + } + + static int GetOffset(TextDocument document, XmlReader reader) + { + IXmlLineInfo info = reader as IXmlLineInfo; + if (info != null && info.HasLineInfo()) { + return document.GetOffset(info.LineNumber, info.LinePosition); + } else { + throw new ArgumentException("XmlReader does not have positioning information."); + } + } + + /// <summary> + /// Creates a comment fold if the comment spans more than one line. + /// </summary> + /// <remarks>The text displayed when the comment is folded is the first + /// line of the comment.</remarks> + static void CreateCommentFold(TextDocument document, List<NewFolding> foldMarkers, XmlReader reader) + { + string comment = reader.Value; + if (comment != null) { + int firstNewLine = comment.IndexOf('\n'); + if (firstNewLine >= 0) { + + // Take off 4 chars to get the actual comment start (takes + // into account the <!-- chars. + + int startOffset = GetOffset(document, reader) - 4; + int endOffset = startOffset + comment.Length + 7; + + string foldText = String.Concat("<!--", comment.Substring(0, firstNewLine).TrimEnd('\r') , "-->"); + foldMarkers.Add(new NewFolding(startOffset, endOffset) { Name = foldText } ); + } + } + } + + /// <summary> + /// Creates an XmlFoldStart for the start tag of an element. + /// </summary> + XmlFoldStart CreateElementFoldStart(TextDocument document, XmlReader reader) + { + // Take off 1 from the offset returned + // from the xml since it points to the start + // of the element name and not the beginning + // tag. + //XmlFoldStart newFoldStart = new XmlFoldStart(reader.Prefix, reader.LocalName, reader.LineNumber - 1, reader.LinePosition - 2); + XmlFoldStart newFoldStart = new XmlFoldStart(); + + IXmlLineInfo lineInfo = (IXmlLineInfo)reader; + newFoldStart.StartLine = lineInfo.LineNumber; + newFoldStart.StartOffset = document.GetOffset(newFoldStart.StartLine, lineInfo.LinePosition - 1); + + if (this.ShowAttributesWhenFolded && reader.HasAttributes) { + newFoldStart.Name = String.Concat("<", reader.Name, " ", GetAttributeFoldText(reader), ">"); + } else { + newFoldStart.Name = String.Concat("<", reader.Name, ">"); + } + + return newFoldStart; + } + + /// <summary> + /// Create an element fold if the start and end tag are on + /// different lines. + /// </summary> + static void CreateElementFold(TextDocument document, List<NewFolding> foldMarkers, XmlReader reader, XmlFoldStart foldStart) + { + IXmlLineInfo lineInfo = (IXmlLineInfo)reader; + int endLine = lineInfo.LineNumber; + if (endLine > foldStart.StartLine) { + int endCol = lineInfo.LinePosition + reader.Name.Length + 1; + foldStart.EndOffset = document.GetOffset(endLine, endCol); + foldMarkers.Add(foldStart); + } + } + + /// <summary> + /// Gets the element's attributes as a string on one line that will + /// be displayed when the element is folded. + /// </summary> + /// <remarks> + /// Currently this puts all attributes from an element on the same + /// line of the start tag. It does not cater for elements where attributes + /// are not on the same line as the start tag. + /// </remarks> + static string GetAttributeFoldText(XmlReader reader) + { + StringBuilder text = new StringBuilder(); + + for (int i = 0; i < reader.AttributeCount; ++i) { + reader.MoveToAttribute(i); + + text.Append(reader.Name); + text.Append("="); + text.Append(reader.QuoteChar.ToString()); + text.Append(XmlEncodeAttributeValue(reader.Value, reader.QuoteChar)); + text.Append(reader.QuoteChar.ToString()); + + // Append a space if this is not the + // last attribute. + if (i < reader.AttributeCount - 1) { + text.Append(" "); + } + } + + return text.ToString(); + } + + /// <summary> + /// Xml encode the attribute string since the string returned from + /// the XmlTextReader is the plain unencoded string and .NET + /// does not provide us with an xml encode method. + /// </summary> + static string XmlEncodeAttributeValue(string attributeValue, char quoteChar) + { + StringBuilder encodedValue = new StringBuilder(attributeValue); + + encodedValue.Replace("&", "&"); + encodedValue.Replace("<", "<"); + encodedValue.Replace(">", ">"); + + if (quoteChar == '"') { + encodedValue.Replace("\"", """); + } else { + encodedValue.Replace("'", "'"); + } + + return encodedValue.ToString(); + } + } +} |
