aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/TextEditorWeakEventManager.cs
blob: 87057ead40fe66c849d272779f312dfdcaf0d80a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 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 Tango.Scripting.Editors.Utils;
using System;

namespace Tango.Scripting.Editors
{
	/// <summary>
	/// Contains weak event managers for <see cref="ITextEditorComponent"/>.
	/// </summary>
	public static class TextEditorWeakEventManager
	{
		/// <summary>
		/// Weak event manager for the <see cref="ITextEditorComponent.DocumentChanged"/> event.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
		public sealed class DocumentChanged : WeakEventManagerBase<DocumentChanged, ITextEditorComponent>
		{
			/// <inheritdoc/>
			protected override void StartListening(ITextEditorComponent source)
			{
				source.DocumentChanged += DeliverEvent;
			}
			
			/// <inheritdoc/>
			protected override void StopListening(ITextEditorComponent source)
			{
				source.DocumentChanged -= DeliverEvent;
			}
		}
		
		/// <summary>
		/// Weak event manager for the <see cref="ITextEditorComponent.OptionChanged"/> event.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
		public sealed class OptionChanged : WeakEventManagerBase<OptionChanged, ITextEditorComponent>
		{
			/// <inheritdoc/>
			protected override void StartListening(ITextEditorComponent source)
			{
				source.OptionChanged += DeliverEvent;
			}
			
			/// <inheritdoc/>
			protected override void StopListening(ITextEditorComponent source)
			{
				source.pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
// 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.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
using Tango.Scripting.Editors.Document;
using Tango.Scripting.Editors.Rendering;
using Tango.Scripting.Editors.Utils;

namespace Tango.Scripting.Editors.Editing
{
	/// <summary>
	/// Rectangular selection ("box selection").
	/// </summary>
	public sealed class RectangleSelection : Selection
	{
		#region Commands
		/// <summary>
		/// Expands the selection left by one character, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Left
		/// </summary>
		public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter");
		
		/// <summary>
		/// Expands the selection right by one character, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Right
		/// </summary>
		public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter");
		
		/// <summary>
		/// Expands the selection left by one word, creating a rectangular selection.
		/// Key gesture: Ctrl+Alt+Shift+Left
		/// </summary>
		public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord");
		
		/// <summary>
		/// Expands the selection left by one word, creating a rectangular selection.
		/// Key gesture: Ctrl+Alt+Shift+Right
		/// </summary>
		public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord");
		
		/// <summary>
		/// Expands the selection up by one line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Up
		/// </summary>
		public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine");
		
		/// <summary>
		/// Expands the selection up by one line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Down
		/// </summary>
		public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine");
		
		/// <summary>
		/// Expands the selection to the start of the line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+Home
		/// </summary>
		public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart");
		
		/// <summary>
		/// Expands the selection to the end of the line, creating a rectangular selection.
		/// Key gesture: Alt+Shift+End
		/// </summary>
		public static readonly RoutedUICommand BoxSelectToLineEnd = Command("BoxSelectToLineEnd");
		
		static RoutedUICommand Command(string name)
		{
			return new RoutedUICommand(name, name, typeof(RectangleSelection));
		}
		#endregion
		
		TextDocument document;
		readonly int startLine, endLine;
		readonly double startXPos, endXPos;
		readonly int topLeftOffset, bottomRightOffset;
		readonly TextViewPosition start, end;
		
		readonly List<SelectionSegment> segments = new List<SelectionSegment>();
		
		#region Constructors
		/// <summary>
		/// Creates a new rectangular selection.
		/// </summary>
		public RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end)
			: base(textArea)
		{
			InitDocument();
			this.startLine = start.Line;
			this.endLine = end.Line;
			this.startXPos = GetXPos(textArea, start);
			this.endXPos = GetXPos(textArea, end);
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = start;
			this.end = end;
		}
		
		private RectangleSelection(TextArea textArea, int startLine, double startXPos, TextViewPosition end)
			: base(textArea)
		{
			InitDocument();
			this.startLine = startLine;
			this.endLine = end.Line;
			this.startXPos = startXPos;
			this.endXPos = GetXPos(textArea, end);
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = GetStart();
			this.end = end;
		}
		
		private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, double endXPos)
			: base(textArea)
		{
			InitDocument();
			this.startLine = start.Line;
			this.endLine = endLine;
			this.startXPos = GetXPos(textArea, start);
			this.endXPos = endXPos;
			CalculateSegments();
			this.topLeftOffset = this.segments.First().StartOffset;
			this.bottomRightOffset = this.segments.Last().EndOffset;
			
			this.start = start;
			this.end = GetEnd();
		}
		
		void InitDocument()
		{
			document = textArea.Document;
			if (document == null)
				throw ThrowUtil.NoDocumentAssigned();
		}
		
		static double GetXPos(TextArea textArea, TextViewPosition pos)
		{
			DocumentLine documentLine = textArea.Document.GetLineByNumber(pos.Line);
			VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine);
			int vc = visualLine.ValidateVisualColumn(pos, true);
			TextLine textLine = visualLine.GetTextLine(vc);
			return visualLine.GetTextLineVisualXPosition(textLine, vc);
		}
		
		void CalculateSegments()
		{
			DocumentLine nextLine = document.GetLineByNumber(Math.Min(startLine, endLine));
			do {
				VisualLine vl = textArea.TextView.GetOrConstructVisualLine(nextLine);
				int startVC = vl.GetVisualColumn(new Point(startXPos, 0), true);
				int endVC = vl.GetVisualColumn(new Point(endXPos, 0), true);
				
				int baseOffset = vl.FirstDocumentLine.Offset;
				int startOffset = baseOffset + vl.GetRelativeOffset(startVC);
				int endOffset = baseOffset + vl.GetRelativeOffset(endVC);
				segments.Add(new SelectionSegment(startOffset, startVC, endOffset, endVC));
				
				nextLine = vl.LastDocumentLine.NextLine;
			} while (nextLine != null && nextLine.LineNumber <= Math.Max(startLine, endLine));
		}
		
		TextViewPosition GetStart()
		{
			SelectionSegment segment = (startLine < endLine ? segments.First() : segments.Last());
			if (startXPos < endXPos) {
				return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
			} else {
				return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
			}
		}
		
		TextViewPosition GetEnd()
		{
			SelectionSegment segment = (startLine < endLine ? segments.Last() : segments.First());
			if (startXPos < endXPos) {
				return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
			} else {
				return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
			}
		}
		#endregion
		
		/// <inheritdoc/>
		public override string GetText()
		{
			StringBuilder b = new StringBuilder();
			foreach (ISegment s in this.Segments) {
				if (b.Length > 0)
					b.AppendLine();
				b.Append(document.GetText(s));
			}
			return b.ToString();
		}
		
		/// <inheritdoc/>
		public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition)
		{
			return SetEndpoint(endPosition);
		}
		
		/// <inheritdoc/>
		public override int Length {
			get {
				return this.Segments.Sum(s => s.Length);
			}
		}
		
		/// <inheritdoc/>
		public override bool EnableVirtualSpace {
			get { return true; }
		}
		
		/// <inheritdoc/>
		public override ISegment SurroundingSegment {
			get {
				return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset);
			}
		}
		
		/// <inheritdoc/>
		public override IEnumerable<SelectionSegment> Segments {
			get { return segments; }
		}
		
		/// <inheritdoc/>
		public override TextViewPosition StartPosition {
			get { return start; }
		}
		
		/// <inheritdoc/>
		public override TextViewPosition EndPosition {
			get { return end; }
		}
		
		/// <inheritdoc/>
		public override bool Equals(object obj)
		{
			RectangleSelection r = obj as RectangleSelection;
			return r != null && r.textArea == this.textArea
				&& r.topLeftOffset == this.topLeftOffset && r.bottomRightOffset == this.bottomRightOffset
				&& r.startLine == this.startLine && r.endLine == this.endLine
				&& r.startXPos == this.startXPos && r.endXPos == this.endXPos;
		}
		
		/// <inheritdoc/>
		public override int GetHashCode()
		{
			return topLeftOffset ^ bottomRightOffset;
		}
		
		/// <inheritdoc/>
		public override Selection SetEndpoint(TextViewPosition endPosition)
		{
			return new RectangleSelection(textArea, startLine, startXPos, endPosition);
		}
		
		int GetVisualColumnFromXPos(int line, double xPos)
		{
			var vl = textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(line));
			return vl.GetVisualColumn(new Point(xPos, 0), true);
		}
		
		/// <inheritdoc/>
		public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e)
		{
			TextLocation newStartLocation = textArea.Document.GetLocation(e.GetNewOffset(topLeftOffset, AnchorMovementType.AfterInsertion));
			TextLocation newEndLocation = textArea.Document.GetLocation(e.GetNewOffset(bottomRightOffset, AnchorMovementType.BeforeInsertion));
			
			return new RectangleSelection(textArea,
			                              new TextViewPosition(newStartLocation, GetVisualColumnFromXPos(newStartLocation.Line, startXPos)),
			                              new TextViewPosition(newEndLocation, GetVisualColumnFromXPos(newEndLocation.Line, endXPos)));
		}
		
		/// <inheritdoc/>
		public override void ReplaceSelectionWithText(string newText)
		{
			if (newText == null)
				throw new ArgumentNullException("newText");
			using (textArea.Document.RunUpdate()) {
				TextViewPosition start = new TextViewPosition(document.GetLocation(topLeftOffset), GetVisualColumnFromXPos(startLine, startXPos));
				TextViewPosition end = new TextViewPosition(document.GetLocation(bottomRightOffset), GetVisualColumnFromXPos(endLine, endXPos));
				int insertionLength;
				int totalInsertionLength = 0;
				int firstInsertionLength = 0;
				int editOffset = Math.Min(topLeftOffset, bottomRightOffset);
				TextViewPosition pos;
				if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) {
					// insert same text into every line
					foreach (SelectionSegment lineSegment in this.Segments.Reverse()) {
						ReplaceSingleLineText(textArea, lineSegment, newText, out insertionLength);
						totalInsertionLength += insertionLength;
						firstInsertionLength = insertionLength;
					}
					
					int newEndOffset = editOffset + totalInsertionLength;
					pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength));
					
					textArea.Selection = new RectangleSelection(textArea, pos, Math.Max(startLine, endLine), GetXPos(textArea, pos));
				} else {
					string[] lines = newText.Split(NewLineFinder.NewlineStrings, segments.Count, StringSplitOptions.None);
					int line = Math.Min(startLine, endLine);
					for (int i = lines.Length - 1; i >= 0; i--) {
						ReplaceSingleLineText(textArea, segments[i], lines[i], out insertionLength);
						firstInsertionLength = insertionLength;
					}
					pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength));
					textArea.ClearSelection();
				}
				textArea.Caret.Position = textArea.TextView.GetPosition(new Point(GetXPos(textArea, pos), textArea.TextView.GetVisualTopByDocumentLine(Math.Max(startLine, endLine)))).GetValueOrDefault();
			}
		}
		
		void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegment, string newText, out int insertionLength)
		{
			if (lineSegment.Length == 0) {
				if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset)) {
					newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn));
					textArea.Document.Insert(lineSegment.StartOffset, newText);
				}
			} else {
				ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment);
				for (int i = segmentsToDelete.Length - 1; i >= 0; i--) {
					if (i == segmentsToDelete.Length - 1) {
						if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) {
							newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn));
						}
						textArea.Document.Replace(segmentsToDelete[i], newText);
					} else {
						textArea.Document.Remove(segmentsToDelete[i]);
					}
				}
			}
			insertionLength = newText.Length;
		}
		
		/// <summary>
		/// Performs a rectangular paste operation.
		/// </summary>
		public static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText)
		{
			if (textArea == null)
				throw new ArgumentNullException("textArea");
			if (text == null)
				throw new ArgumentNullException("text");
			int newLineCount = text.Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today.
			TextLocation endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column);
			if (endLocation.Line <= textArea.Document.LineCount) {
				int endOffset = textArea.Document.GetOffset(endLocation);
				if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation) {
					RectangleSelection rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition));
					rsel.ReplaceSelectionWithText(text);
					if (selectInsertedText && textArea.Selection is RectangleSelection) {
						RectangleSelection sel = (RectangleSelection)textArea.Selection;
						textArea.Selection = new RectangleSelection(textArea, startPosition, sel.endLine, sel.endXPos);
					}
					return true;
				}
			}
			return false;
		}
		
		/// <summary>
		/// Gets the name of the entry in the DataObject that signals rectangle selections.
		/// </summary>
		public const string RectangularSelectionDataType = "AvalonEditRectangularSelection";
		
		/// <inheritdoc/>
		public override System.Windows.DataObject CreateDataObject(TextArea textArea)
		{
			var data = base.CreateDataObject(textArea);
			
			MemoryStream isRectangle = new MemoryStream(1);
			isRectangle.WriteByte(1);
			data.SetData(RectangularSelectionDataType, isRectangle, false);
			return data;
		}
		
		/// <inheritdoc/>
		public override string ToString()
		{
			// It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message
			// make sure we don't crash even when the desired locations don't exist anymore.
			return string.Format("[RectangleSelection {0} {1} {2} to {3} {4} {5}]", startLine, topLeftOffset, startXPos, endLine, bottomRightOffset, endXPos);
		}
	}
}