diff options
| author | Victoria Plitt <Victoria.Plitt@twine-s.com> | 2019-04-08 13:49:55 +0300 |
|---|---|---|
| committer | Victoria Plitt <Victoria.Plitt@twine-s.com> | 2019-04-08 13:49:55 +0300 |
| commit | fc8a05358a92cc3c77c5f1e30d536807ef0614fd (patch) | |
| tree | c65f696ebd60f3790145721307c255e5a339923f /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets | |
| parent | b4a71931ea52636c6b36376aa9d71697ccf73524 (diff) | |
| download | Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.tar.gz Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.zip | |
were added scripting projects
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets')
13 files changed, 1143 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/IActiveElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/IActiveElement.cs new file mode 100644 index 000000000..07275f273 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/IActiveElement.cs @@ -0,0 +1,35 @@ +// 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.Media; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Represents an active element that allows the snippet to stay interactive after insertion. + /// </summary> + public interface IActiveElement + { + /// <summary> + /// Called when the all snippet elements have been inserted. + /// </summary> + void OnInsertionCompleted(); + + /// <summary> + /// Called when the interactive mode is deactivated. + /// </summary> + void Deactivate(SnippetEventArgs e); + + /// <summary> + /// Gets whether this element is editable (the user will be able to select it with Tab). + /// </summary> + bool IsEditable { get; } + + /// <summary> + /// Gets the segment associated with this element. May be null. + /// </summary> + ISegment Segment { get; } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/InsertionContext.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/InsertionContext.cs new file mode 100644 index 000000000..d7de8311d --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/InsertionContext.cs @@ -0,0 +1,269 @@ +// 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 Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Represents the context of a snippet insertion. + /// </summary> + public class InsertionContext : IWeakEventListener + { + enum Status + { + Insertion, + RaisingInsertionCompleted, + Interactive, + RaisingDeactivated, + Deactivated + } + + Status currentStatus = Status.Insertion; + + /// <summary> + /// Creates a new InsertionContext instance. + /// </summary> + public InsertionContext(TextArea textArea, int insertionPosition) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.TextArea = textArea; + this.Document = textArea.Document; + this.SelectedText = textArea.Selection.GetText(); + this.InsertionPosition = insertionPosition; + this.startPosition = insertionPosition; + + DocumentLine startLine = this.Document.GetLineByOffset(insertionPosition); + ISegment indentation = TextUtilities.GetWhitespaceAfter(this.Document, startLine.Offset); + this.Indentation = Document.GetText(indentation.Offset, Math.Min(indentation.EndOffset, insertionPosition) - indentation.Offset); + this.Tab = textArea.Options.IndentationString; + + this.LineTerminator = TextUtilities.GetNewLineFromDocument(this.Document, startLine.LineNumber); + } + + /// <summary> + /// Gets the text area. + /// </summary> + public TextArea TextArea { get; private set; } + + /// <summary> + /// Gets the text document. + /// </summary> + public TextDocument Document { get; private set; } + + /// <summary> + /// Gets the text that was selected before the insertion of the snippet. + /// </summary> + public string SelectedText { get; private set; } + + /// <summary> + /// Gets the indentation at the insertion position. + /// </summary> + public string Indentation { get; private set; } + + /// <summary> + /// Gets the indentation string for a single indentation level. + /// </summary> + public string Tab { get; private set; } + + /// <summary> + /// Gets the line terminator at the insertion position. + /// </summary> + public string LineTerminator { get; private set; } + + /// <summary> + /// Gets/Sets the insertion position. + /// </summary> + public int InsertionPosition { get; set; } + + readonly int startPosition; + AnchorSegment wholeSnippetAnchor; + bool deactivateIfSnippetEmpty; + + /// <summary> + /// Gets the start position of the snippet insertion. + /// </summary> + public int StartPosition { + get { + if (wholeSnippetAnchor != null) + return wholeSnippetAnchor.Offset; + else + return startPosition; + } + } + + /// <summary> + /// Inserts text at the insertion position and advances the insertion position. + /// This method will add the current indentation to every line in <paramref name="text"/> and will + /// replace newlines with the expected newline for the document. + /// </summary> + public void InsertText(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + if (currentStatus != Status.Insertion) + throw new InvalidOperationException(); + + text = text.Replace("\t", this.Tab); + + using (this.Document.RunUpdate()) { + int textOffset = 0; + SimpleSegment segment; + while ((segment = NewLineFinder.NextNewLine(text, textOffset)) != SimpleSegment.Invalid) { + string insertString = text.Substring(textOffset, segment.Offset - textOffset) + + this.LineTerminator + this.Indentation; + this.Document.Insert(InsertionPosition, insertString); + this.InsertionPosition += insertString.Length; + textOffset = segment.EndOffset; + } + string remainingInsertString = text.Substring(textOffset); + this.Document.Insert(InsertionPosition, remainingInsertString); + this.InsertionPosition += remainingInsertString.Length; + } + } + + Dictionary<SnippetElement, IActiveElement> elementMap = new Dictionary<SnippetElement, IActiveElement>(); + List<IActiveElement> registeredElements = new List<IActiveElement>(); + + /// <summary> + /// Registers an active element. Elements should be registered during insertion and will be called back + /// when insertion has completed. + /// </summary> + /// <param name="owner">The snippet element that created the active element.</param> + /// <param name="element">The active element.</param> + public void RegisterActiveElement(SnippetElement owner, IActiveElement element) + { + if (owner == null) + throw new ArgumentNullException("owner"); + if (element == null) + throw new ArgumentNullException("element"); + if (currentStatus != Status.Insertion) + throw new InvalidOperationException(); + elementMap.Add(owner, element); + registeredElements.Add(element); + } + + /// <summary> + /// Returns the active element belonging to the specified snippet element, or null if no such active element is found. + /// </summary> + public IActiveElement GetActiveElement(SnippetElement owner) + { + if (owner == null) + throw new ArgumentNullException("owner"); + IActiveElement element; + if (elementMap.TryGetValue(owner, out element)) + return element; + else + return null; + } + + /// <summary> + /// Gets the list of active elements. + /// </summary> + public IEnumerable<IActiveElement> ActiveElements { + get { return registeredElements; } + } + + /// <summary> + /// Calls the <see cref="IActiveElement.OnInsertionCompleted"/> method on all registered active elements + /// and raises the <see cref="InsertionCompleted"/> event. + /// </summary> + /// <param name="e">The EventArgs to use</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", + Justification="There is an event and this method is raising it.")] + public void RaiseInsertionCompleted(EventArgs e) + { + if (currentStatus != Status.Insertion) + throw new InvalidOperationException(); + if (e == null) + e = EventArgs.Empty; + + currentStatus = Status.RaisingInsertionCompleted; + int endPosition = this.InsertionPosition; + this.wholeSnippetAnchor = new AnchorSegment(Document, startPosition, endPosition - startPosition); + TextDocumentWeakEventManager.UpdateFinished.AddListener(Document, this); + deactivateIfSnippetEmpty = (endPosition != startPosition); + + foreach (IActiveElement element in registeredElements) { + element.OnInsertionCompleted(); + } + if (InsertionCompleted != null) + InsertionCompleted(this, e); + currentStatus = Status.Interactive; + if (registeredElements.Count == 0) { + // deactivate immediately if there are no interactive elements + Deactivate(new SnippetEventArgs(DeactivateReason.NoActiveElements)); + } else { + myInputHandler = new SnippetInputHandler(this); + // disable existing snippet input handlers - there can be only 1 active snippet + foreach (TextAreaStackedInputHandler h in TextArea.StackedInputHandlers) { + if (h is SnippetInputHandler) + TextArea.PopStackedInputHandler(h); + } + TextArea.PushStackedInputHandler(myInputHandler); + } + } + + SnippetInputHandler myInputHandler; + + /// <summary> + /// Occurs when the all snippet elements have been inserted. + /// </summary> + public event EventHandler InsertionCompleted; + + /// <summary> + /// Calls the <see cref="IActiveElement.Deactivate"/> method on all registered active elements. + /// </summary> + /// <param name="e">The EventArgs to use</param> + public void Deactivate(SnippetEventArgs e) + { + if (currentStatus == Status.Deactivated || currentStatus == Status.RaisingDeactivated) + return; + if (currentStatus != Status.Interactive) + throw new InvalidOperationException("Cannot call Deactivate() until RaiseInsertionCompleted() has finished."); + if (e == null) + e = new SnippetEventArgs(DeactivateReason.Unknown); + + TextDocumentWeakEventManager.UpdateFinished.RemoveListener(Document, this); + currentStatus = Status.RaisingDeactivated; + TextArea.PopStackedInputHandler(myInputHandler); + foreach (IActiveElement element in registeredElements) { + element.Deactivate(e); + } + if (Deactivated != null) + Deactivated(this, e); + currentStatus = Status.Deactivated; + } + + /// <summary> + /// Occurs when the interactive mode is deactivated. + /// </summary> + public event EventHandler<SnippetEventArgs> Deactivated; + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + return ReceiveWeakEvent(managerType, sender, e); + } + + /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/> + protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(TextDocumentWeakEventManager.UpdateFinished)) { + // Deactivate if snippet is deleted. This is necessary for correctly leaving interactive + // mode if Undo is pressed after a snippet insertion. + if (wholeSnippetAnchor.Length == 0 && deactivateIfSnippetEmpty) + Deactivate(new SnippetEventArgs(DeactivateReason.Deleted)); + return true; + } + return false; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/Snippet.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/Snippet.cs new file mode 100644 index 000000000..0006261fb --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/Snippet.cs @@ -0,0 +1,47 @@ +// 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.Documents; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// A code snippet that can be inserted into the text editor. + /// </summary> + [Serializable] + public class Snippet : SnippetContainerElement + { + /// <summary> + /// Inserts the snippet into the text area. + /// </summary> + public void Insert(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + + ISegment selection = textArea.Selection.SurroundingSegment; + int insertionPosition = textArea.Caret.Offset; + + if (selection != null) // if something is selected + // use selection start instead of caret position, + // because caret could be at end of selection or anywhere inside. + // Removal of the selected text causes the caret position to be invalid. + insertionPosition = selection.Offset + TextUtilities.GetWhitespaceAfter(textArea.Document, selection.Offset).Length; + + InsertionContext context = new InsertionContext(textArea, insertionPosition); + + using (context.Document.RunUpdate()) { + if (selection != null) + textArea.Document.Remove(insertionPosition, selection.EndOffset - insertionPosition); + Insert(context); + context.RaiseInsertionCompleted(EventArgs.Empty); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetAnchorElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetAnchorElement.cs new file mode 100644 index 000000000..cfce03b6a --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetAnchorElement.cs @@ -0,0 +1,97 @@ +// 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; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Creates a named anchor that can be accessed by other SnippetElements. + /// </summary> + public sealed class SnippetAnchorElement : SnippetElement + { + /// <summary> + /// Gets or sets the name of the anchor. + /// </summary> + public string Name { get; private set; } + + /// <summary> + /// Creates a SnippetAnchorElement with the supplied name. + /// </summary> + public SnippetAnchorElement(string name) + { + this.Name = name; + } + + /// <inheritdoc /> + public override void Insert(InsertionContext context) + { + TextAnchor start = context.Document.CreateAnchor(context.InsertionPosition); + start.MovementType = AnchorMovementType.BeforeInsertion; + start.SurviveDeletion = true; + AnchorSegment segment = new AnchorSegment(start, start); + context.RegisterActiveElement(this, new AnchorElement(segment, Name, context)); + } + } + + /// <summary> + /// AnchorElement created by SnippetAnchorElement. + /// </summary> + public sealed class AnchorElement : IActiveElement + { + /// <inheritdoc /> + public bool IsEditable { + get { return false; } + } + + AnchorSegment segment; + InsertionContext context; + + /// <inheritdoc /> + public ISegment Segment { + get { return segment; } + } + + /// <summary> + /// Creates a new AnchorElement. + /// </summary> + public AnchorElement(AnchorSegment segment, string name, InsertionContext context) + { + this.segment = segment; + this.context = context; + this.Name = name; + } + + /// <summary> + /// Gets or sets the text at the anchor. + /// </summary> + public string Text { + get { return context.Document.GetText(segment); } + set { + int offset = segment.Offset; + int length = segment.Length; + context.Document.Replace(offset, length, value); + if (length == 0) { + // replacing an empty anchor segment with text won't enlarge it, so we have to recreate it + segment = new AnchorSegment(context.Document, offset, value.Length); + } + } + } + + /// <summary> + /// Gets or sets the name of the anchor. + /// </summary> + public string Name { get; private set; } + + /// <inheritdoc /> + public void OnInsertionCompleted() + { + } + + /// <inheritdoc /> + public void Deactivate(SnippetEventArgs e) + { + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetBoundElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetBoundElement.cs new file mode 100644 index 000000000..92f6d4fd4 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetBoundElement.cs @@ -0,0 +1,122 @@ +// 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.Documents; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// An element that binds to a <see cref="SnippetReplaceableTextElement"/> and displays the same text. + /// </summary> + [Serializable] + public class SnippetBoundElement : SnippetElement + { + SnippetReplaceableTextElement targetElement; + + /// <summary> + /// Gets/Sets the target element. + /// </summary> + public SnippetReplaceableTextElement TargetElement { + get { return targetElement; } + set { targetElement = value; } + } + + /// <summary> + /// Converts the text before copying it. + /// </summary> + public virtual string ConvertText(string input) + { + return input; + } + + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + if (targetElement != null) { + TextAnchor start = context.Document.CreateAnchor(context.InsertionPosition); + start.MovementType = AnchorMovementType.BeforeInsertion; + start.SurviveDeletion = true; + string inputText = targetElement.Text; + if (inputText != null) { + context.InsertText(ConvertText(inputText)); + } + TextAnchor end = context.Document.CreateAnchor(context.InsertionPosition); + end.MovementType = AnchorMovementType.BeforeInsertion; + end.SurviveDeletion = true; + AnchorSegment segment = new AnchorSegment(start, end); + context.RegisterActiveElement(this, new BoundActiveElement(context, targetElement, this, segment)); + } + } + + /// <inheritdoc/> + public override Inline ToTextRun() + { + if (targetElement != null) { + string inputText = targetElement.Text; + if (inputText != null) { + return new Italic(new Run(ConvertText(inputText))); + } + } + return base.ToTextRun(); + } + } + + sealed class BoundActiveElement : IActiveElement + { + InsertionContext context; + SnippetReplaceableTextElement targetSnippetElement; + SnippetBoundElement boundElement; + internal IReplaceableActiveElement targetElement; + AnchorSegment segment; + + public BoundActiveElement(InsertionContext context, SnippetReplaceableTextElement targetSnippetElement, SnippetBoundElement boundElement, AnchorSegment segment) + { + this.context = context; + this.targetSnippetElement = targetSnippetElement; + this.boundElement = boundElement; + this.segment = segment; + } + + public void OnInsertionCompleted() + { + targetElement = context.GetActiveElement(targetSnippetElement) as IReplaceableActiveElement; + if (targetElement != null) { + targetElement.TextChanged += targetElement_TextChanged; + } + } + + void targetElement_TextChanged(object sender, EventArgs e) + { + // Don't copy text if the segments overlap (we would get an endless loop). + // This can happen if the user deletes the text between the replaceable element and the bound element. + if (segment.GetOverlap(targetElement.Segment) == SimpleSegment.Invalid) { + int offset = segment.Offset; + int length = segment.Length; + string text = boundElement.ConvertText(targetElement.Text); + if (length != text.Length || text != context.Document.GetText(offset, length)) { + // Call replace only if we're actually changing something. + // Without this check, we would generate an empty undo group when the user pressed undo. + context.Document.Replace(offset, length, text); + if (length == 0) { + // replacing an empty anchor segment with text won't enlarge it, so we have to recreate it + segment = new AnchorSegment(context.Document, offset, text.Length); + } + } + } + } + + public void Deactivate(SnippetEventArgs e) + { + } + + public bool IsEditable { + get { return false; } + } + + public ISegment Segment { + get { return segment; } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetCaretElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetCaretElement.cs new file mode 100644 index 000000000..1b33abf66 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetCaretElement.cs @@ -0,0 +1,58 @@ +// 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.Runtime.Serialization; +using System.Windows.Input; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Sets the caret position after interactive mode has finished. + /// </summary> + [Serializable] + public class SnippetCaretElement : SnippetElement + { + [OptionalField] + bool setCaretOnlyIfTextIsSelected; + + /// <summary> + /// Creates a new SnippetCaretElement. + /// </summary> + public SnippetCaretElement() + { + } + + /// <summary> + /// Creates a new SnippetCaretElement. + /// </summary> + /// <param name="setCaretOnlyIfTextIsSelected"> + /// If set to true, the caret is set only when some text was selected. + /// This is useful when both SnippetCaretElement and SnippetSelectionElement are used in the same snippet. + /// </param> + public SnippetCaretElement(bool setCaretOnlyIfTextIsSelected) + { + this.setCaretOnlyIfTextIsSelected = setCaretOnlyIfTextIsSelected; + } + + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + if (!setCaretOnlyIfTextIsSelected || !string.IsNullOrEmpty(context.SelectedText)) + SetCaret(context); + } + + internal static void SetCaret(InsertionContext context) + { + TextAnchor pos = context.Document.CreateAnchor(context.InsertionPosition); + pos.MovementType = AnchorMovementType.BeforeInsertion; + pos.SurviveDeletion = true; + context.Deactivated += (sender, e) => { + if (e.Reason == DeactivateReason.ReturnPressed || e.Reason == DeactivateReason.NoActiveElements) { + context.TextArea.Caret.Offset = pos.Offset; + } + }; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetContainerElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetContainerElement.cs new file mode 100644 index 000000000..066f1a870 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetContainerElement.cs @@ -0,0 +1,49 @@ +// 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.Text; +using System.Windows.Documents; + +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// A snippet element that has sub-elements. + /// </summary> + [Serializable] + public class SnippetContainerElement : SnippetElement + { + NullSafeCollection<SnippetElement> elements = new NullSafeCollection<SnippetElement>(); + + /// <summary> + /// Gets the list of child elements. + /// </summary> + public IList<SnippetElement> Elements { + get { return elements; } + } + + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + foreach (SnippetElement e in this.Elements) { + e.Insert(context); + } + } + + /// <inheritdoc/> + public override Inline ToTextRun() + { + Span span = new Span(); + foreach (SnippetElement e in this.Elements) { + Inline r = e.ToTextRun(); + if (r != null) + span.Inlines.Add(r); + } + return span; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetElement.cs new file mode 100644 index 000000000..2e2c4556e --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetElement.cs @@ -0,0 +1,32 @@ +// 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.Documents; + +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// An element inside a snippet. + /// </summary> + [Serializable] + public abstract class SnippetElement + { + /// <summary> + /// Performs insertion of the snippet. + /// </summary> + public abstract void Insert(InsertionContext context); + + /// <summary> + /// Converts the snippet to text, with replaceable fields in italic. + /// </summary> + public virtual Inline ToTextRun() + { + return null; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetEventArgs.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetEventArgs.cs new file mode 100644 index 000000000..cfc993485 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetEventArgs.cs @@ -0,0 +1,57 @@ +// 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; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Provides information about the event that occured during use of snippets. + /// </summary> + public class SnippetEventArgs : EventArgs + { + /// <summary> + /// Gets the reason for deactivation. + /// </summary> + public DeactivateReason Reason { get; private set; } + + /// <summary> + /// Creates a new SnippetEventArgs object, with a DeactivateReason. + /// </summary> + public SnippetEventArgs(DeactivateReason reason) + { + this.Reason = reason; + } + } + + /// <summary> + /// Describes the reason for deactivation of a <see cref="SnippetElement" />. + /// </summary> + public enum DeactivateReason + { + /// <summary> + /// Unknown reason. + /// </summary> + Unknown, + /// <summary> + /// Snippet was deleted. + /// </summary> + Deleted, + /// <summary> + /// There are no active elements in the snippet. + /// </summary> + NoActiveElements, + /// <summary> + /// The SnippetInputHandler was detached. + /// </summary> + InputHandlerDetached, + /// <summary> + /// Return was pressed by the user. + /// </summary> + ReturnPressed, + /// <summary> + /// Escape was pressed by the user. + /// </summary> + EscapePressed + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetInputHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetInputHandler.cs new file mode 100644 index 000000000..6a8de2e88 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetInputHandler.cs @@ -0,0 +1,80 @@ +// 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.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows.Input; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; + +namespace Tango.Scripting.Editors.Snippets +{ + sealed class SnippetInputHandler : TextAreaStackedInputHandler + { + readonly InsertionContext context; + + public SnippetInputHandler(InsertionContext context) + : base(context.TextArea) + { + this.context = context; + } + + public override void Attach() + { + base.Attach(); + + SelectElement(FindNextEditableElement(-1, false)); + } + + public override void Detach() + { + base.Detach(); + context.Deactivate(new SnippetEventArgs(DeactivateReason.InputHandlerDetached)); + } + + public override void OnPreviewKeyDown(KeyEventArgs e) + { + base.OnPreviewKeyDown(e); + if (e.Key == Key.Escape) { + context.Deactivate(new SnippetEventArgs(DeactivateReason.EscapePressed)); + e.Handled = true; + } else if (e.Key == Key.Return) { + context.Deactivate(new SnippetEventArgs(DeactivateReason.ReturnPressed)); + e.Handled = true; + } else if (e.Key == Key.Tab) { + bool backwards = e.KeyboardDevice.Modifiers == ModifierKeys.Shift; + SelectElement(FindNextEditableElement(TextArea.Caret.Offset, backwards)); + e.Handled = true; + } + } + + void SelectElement(IActiveElement element) + { + if (element != null) { + TextArea.Selection = Selection.Create(TextArea, element.Segment); + TextArea.Caret.Offset = element.Segment.EndOffset; + } + } + + IActiveElement FindNextEditableElement(int offset, bool backwards) + { + IEnumerable<IActiveElement> elements = context.ActiveElements.Where(e => e.IsEditable && e.Segment != null); + if (backwards) { + elements = elements.Reverse(); + foreach (IActiveElement element in elements) { + if (offset > element.Segment.EndOffset) + return element; + } + } else { + foreach (IActiveElement element in elements) { + if (offset < element.Segment.Offset) + return element; + } + } + return elements.FirstOrDefault(); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetReplaceableTextElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetReplaceableTextElement.cs new file mode 100644 index 000000000..9bf872f11 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetReplaceableTextElement.cs @@ -0,0 +1,213 @@ +// 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.Linq; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Threading; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Text element that is supposed to be replaced by the user. + /// Will register an <see cref="IReplaceableActiveElement"/>. + /// </summary> + [Serializable] + public class SnippetReplaceableTextElement : SnippetTextElement + { + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + int start = context.InsertionPosition; + base.Insert(context); + int end = context.InsertionPosition; + context.RegisterActiveElement(this, new ReplaceableActiveElement(context, start, end)); + } + + /// <inheritdoc/> + public override Inline ToTextRun() + { + return new Italic(base.ToTextRun()); + } + } + + /// <summary> + /// Interface for active element registered by <see cref="SnippetReplaceableTextElement"/>. + /// </summary> + public interface IReplaceableActiveElement : IActiveElement + { + /// <summary> + /// Gets the current text inside the element. + /// </summary> + string Text { get; } + + /// <summary> + /// Occurs when the text inside the element changes. + /// </summary> + event EventHandler TextChanged; + } + + sealed class ReplaceableActiveElement : IReplaceableActiveElement, IWeakEventListener + { + readonly InsertionContext context; + readonly int startOffset, endOffset; + TextAnchor start, end; + + public ReplaceableActiveElement(InsertionContext context, int startOffset, int endOffset) + { + this.context = context; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + void AnchorDeleted(object sender, EventArgs e) + { + context.Deactivate(new SnippetEventArgs(DeactivateReason.Deleted)); + } + + public void OnInsertionCompleted() + { + // anchors must be created in OnInsertionCompleted because they should move only + // due to user insertions, not due to insertions of other snippet parts + start = context.Document.CreateAnchor(startOffset); + start.MovementType = AnchorMovementType.BeforeInsertion; + end = context.Document.CreateAnchor(endOffset); + end.MovementType = AnchorMovementType.AfterInsertion; + start.Deleted += AnchorDeleted; + end.Deleted += AnchorDeleted; + + // Be careful with references from the document to the editing/snippet layer - use weak events + // to prevent memory leaks when the text area control gets dropped from the UI while the snippet is active. + // The InsertionContext will keep us alive as long as the snippet is in interactive mode. + TextDocumentWeakEventManager.TextChanged.AddListener(context.Document, this); + + background = new Renderer { Layer = KnownLayer.Background, element = this }; + foreground = new Renderer { Layer = KnownLayer.Text, element = this }; + context.TextArea.TextView.BackgroundRenderers.Add(background); + context.TextArea.TextView.BackgroundRenderers.Add(foreground); + context.TextArea.Caret.PositionChanged += Caret_PositionChanged; + Caret_PositionChanged(null, null); + + this.Text = GetText(); + } + + public void Deactivate(SnippetEventArgs e) + { + TextDocumentWeakEventManager.TextChanged.RemoveListener(context.Document, this); + context.TextArea.TextView.BackgroundRenderers.Remove(background); + context.TextArea.TextView.BackgroundRenderers.Remove(foreground); + context.TextArea.Caret.PositionChanged -= Caret_PositionChanged; + } + + bool isCaretInside; + + void Caret_PositionChanged(object sender, EventArgs e) + { + ISegment s = this.Segment; + if (s != null) { + bool newIsCaretInside = s.Contains(context.TextArea.Caret.Offset); + if (newIsCaretInside != isCaretInside) { + isCaretInside = newIsCaretInside; + context.TextArea.TextView.InvalidateLayer(foreground.Layer); + } + } + } + + Renderer background, foreground; + + public string Text { get; private set; } + + string GetText() + { + if (start.IsDeleted || end.IsDeleted) + return string.Empty; + else + return context.Document.GetText(start.Offset, Math.Max(0, end.Offset - start.Offset)); + } + + public event EventHandler TextChanged; + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(TextDocumentWeakEventManager.TextChanged)) { + string newText = GetText(); + if (this.Text != newText) { + this.Text = newText; + if (TextChanged != null) + TextChanged(this, e); + } + return true; + } + return false; + } + + public bool IsEditable { + get { return true; } + } + + public ISegment Segment { + get { + if (start.IsDeleted || end.IsDeleted) + return null; + else + return new SimpleSegment(start.Offset, Math.Max(0, end.Offset - start.Offset)); + } + } + + sealed class Renderer : IBackgroundRenderer + { + static readonly Brush backgroundBrush = CreateBackgroundBrush(); + static readonly Pen activeBorderPen = CreateBorderPen(); + + static Brush CreateBackgroundBrush() + { + SolidColorBrush b = new SolidColorBrush(Colors.LimeGreen); + b.Opacity = 0.4; + b.Freeze(); + return b; + } + + static Pen CreateBorderPen() + { + Pen p = new Pen(Brushes.Black, 1); + p.DashStyle = DashStyles.Dot; + p.Freeze(); + return p; + } + + internal ReplaceableActiveElement element; + + public KnownLayer Layer { get; set; } + + public void Draw(TextView textView, System.Windows.Media.DrawingContext drawingContext) + { + ISegment s = element.Segment; + if (s != null) { + BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder(); + geoBuilder.AlignToMiddleOfPixels = true; + if (Layer == KnownLayer.Background) { + geoBuilder.AddSegment(textView, s); + drawingContext.DrawGeometry(backgroundBrush, null, geoBuilder.CreateGeometry()); + } else { + // draw foreground only if active + if (element.isCaretInside) { + geoBuilder.AddSegment(textView, s); + foreach (BoundActiveElement boundElement in element.context.ActiveElements.OfType<BoundActiveElement>()) { + if (boundElement.targetElement == element) { + geoBuilder.AddSegment(textView, boundElement.Segment); + geoBuilder.CloseFigure(); + } + } + drawingContext.DrawGeometry(null, activeBorderPen, geoBuilder.CreateGeometry()); + } + } + } + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetSelectionElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetSelectionElement.cs new file mode 100644 index 000000000..4c073094b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetSelectionElement.cs @@ -0,0 +1,45 @@ +// 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.Text; +using System.Windows.Input; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Inserts the previously selected text at the selection marker. + /// </summary> + [Serializable] + public class SnippetSelectionElement : SnippetElement + { + /// <summary> + /// Gets/Sets the new indentation of the selected text. + /// </summary> + public int Indentation { get; set; } + + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + StringBuilder tabString = new StringBuilder(); + + for (int i = 0; i < Indentation; i++) { + tabString.Append(context.Tab); + } + + string indent = tabString.ToString(); + + string text = context.SelectedText.TrimStart(' ', '\t'); + + text = text.Replace(context.LineTerminator, + context.LineTerminator + indent); + + context.Document.Insert(context.InsertionPosition, text); + context.InsertionPosition += text.Length; + + if (string.IsNullOrEmpty(context.SelectedText)) + SnippetCaretElement.SetCaret(context); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetTextElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetTextElement.cs new file mode 100644 index 000000000..decc18119 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Snippets/SnippetTextElement.cs @@ -0,0 +1,39 @@ +// 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.Documents; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Snippets +{ + /// <summary> + /// Represents a text element in a snippet. + /// </summary> + [Serializable] + public class SnippetTextElement : SnippetElement + { + string text; + + /// <summary> + /// The text to be inserted. + /// </summary> + public string Text { + get { return text; } + set { text = value; } + } + + /// <inheritdoc/> + public override void Insert(InsertionContext context) + { + if (text != null) + context.InsertText(text); + } + + /// <inheritdoc/> + public override Inline ToTextRun() + { + return new Run(text ?? string.Empty); + } + } +} |
