415 lines
18 KiB
C#
415 lines
18 KiB
C#
|
|
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
|||
|
|
//
|
|||
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|||
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|||
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|||
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|||
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|||
|
|
//
|
|||
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|||
|
|
// substantial portions of the Software.
|
|||
|
|
//
|
|||
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|||
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|||
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|||
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|||
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|||
|
|
// DEALINGS IN THE SOFTWARE.
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Text;
|
|||
|
|
using Unity.CodeEditor.Document;
|
|||
|
|
using Unity.CodeEditor.Utils;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
namespace Unity.CodeEditor.Editing
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// Rectangular selection ("box selection").
|
|||
|
|
/// </summary>
|
|||
|
|
internal sealed class RectangleSelection : Selection
|
|||
|
|
{
|
|||
|
|
/*
|
|||
|
|
#region Commands
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection left by one character, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+Left
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectLeftByCharacter = new RoutedCommand(nameof(BoxSelectLeftByCharacter));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection right by one character, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+Right
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectRightByCharacter = new RoutedCommand(nameof(BoxSelectRightByCharacter));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection left by one word, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Ctrl+Alt+Shift+Left
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectLeftByWord = new RoutedCommand(nameof(BoxSelectLeftByWord));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection right by one word, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Ctrl+Alt+Shift+Right
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectRightByWord = new RoutedCommand(nameof(BoxSelectRightByWord));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection up by one line, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+Up
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectUpByLine = new RoutedCommand(nameof(BoxSelectUpByLine));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection down by one line, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+Down
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectDownByLine = new RoutedCommand(nameof(BoxSelectDownByLine));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection to the start of the line, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+Home
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectToLineStart = new RoutedCommand(nameof(BoxSelectToLineStart));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Expands the selection to the end of the line, creating a rectangular selection.
|
|||
|
|
/// Key gesture: Alt+Shift+End
|
|||
|
|
/// </summary>
|
|||
|
|
internal static readonly RoutedCommand BoxSelectToLineEnd = new RoutedCommand(nameof(BoxSelectToLineEnd));
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
private TextDocument _document;
|
|||
|
|
private readonly int _startLine;
|
|||
|
|
private readonly int _endLine;
|
|||
|
|
private readonly float _startXPos;
|
|||
|
|
private readonly float _endXPos;
|
|||
|
|
private readonly int _topLeftOffset;
|
|||
|
|
private readonly int _bottomRightOffset;
|
|||
|
|
|
|||
|
|
private readonly List<SelectionSegment> _segments = new List<SelectionSegment>();
|
|||
|
|
|
|||
|
|
#region Constructors
|
|||
|
|
/// <summary>
|
|||
|
|
/// Creates a new rectangular selection.
|
|||
|
|
/// </summary>
|
|||
|
|
internal RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end)
|
|||
|
|
: base(textArea)
|
|||
|
|
{
|
|||
|
|
InitDocument();
|
|||
|
|
_startLine = start.Line;
|
|||
|
|
_endLine = end.Line;
|
|||
|
|
_startXPos = GetXPos(textArea, start);
|
|||
|
|
_endXPos = GetXPos(textArea, end);
|
|||
|
|
CalculateSegments();
|
|||
|
|
_topLeftOffset = _segments.First().StartOffset;
|
|||
|
|
_bottomRightOffset = _segments.Last().EndOffset;
|
|||
|
|
|
|||
|
|
StartPosition = start;
|
|||
|
|
EndPosition = end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private RectangleSelection(TextArea textArea, int startLine, float startXPos, TextViewPosition end)
|
|||
|
|
: base(textArea)
|
|||
|
|
{
|
|||
|
|
InitDocument();
|
|||
|
|
_startLine = startLine;
|
|||
|
|
_endLine = end.Line;
|
|||
|
|
_startXPos = startXPos;
|
|||
|
|
_endXPos = GetXPos(textArea, end);
|
|||
|
|
CalculateSegments();
|
|||
|
|
_topLeftOffset = _segments.First().StartOffset;
|
|||
|
|
_bottomRightOffset = _segments.Last().EndOffset;
|
|||
|
|
|
|||
|
|
StartPosition = GetStart();
|
|||
|
|
EndPosition = end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, float endXPos)
|
|||
|
|
: base(textArea)
|
|||
|
|
{
|
|||
|
|
InitDocument();
|
|||
|
|
_startLine = start.Line;
|
|||
|
|
_endLine = endLine;
|
|||
|
|
_startXPos = GetXPos(textArea, start);
|
|||
|
|
_endXPos = endXPos;
|
|||
|
|
CalculateSegments();
|
|||
|
|
_topLeftOffset = _segments.First().StartOffset;
|
|||
|
|
_bottomRightOffset = _segments.Last().EndOffset;
|
|||
|
|
|
|||
|
|
StartPosition = start;
|
|||
|
|
EndPosition = GetEnd();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void InitDocument()
|
|||
|
|
{
|
|||
|
|
_document = TextArea.Document;
|
|||
|
|
if (_document == null)
|
|||
|
|
throw ThrowUtil.NoDocumentAssigned();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static float GetXPos(TextArea textArea, TextViewPosition pos)
|
|||
|
|
{
|
|||
|
|
var documentLine = textArea.Document.GetLineByNumber(pos.Line);
|
|||
|
|
var visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine);
|
|||
|
|
var vc = visualLine.ValidateVisualColumn(pos, true);
|
|||
|
|
var textLine = visualLine.GetTextLine(vc, pos.IsAtEndOfLine);
|
|||
|
|
return visualLine.GetTextLineVisualXPosition(textLine, vc);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void CalculateSegments()
|
|||
|
|
{
|
|||
|
|
var nextLine = _document.GetLineByNumber(Math.Min(_startLine, _endLine));
|
|||
|
|
do
|
|||
|
|
{
|
|||
|
|
var vl = TextArea.TextView.GetOrConstructVisualLine(nextLine);
|
|||
|
|
var startVc = vl.GetVisualColumn(new Vector2(_startXPos, 0), true);
|
|||
|
|
var endVc = vl.GetVisualColumn(new Vector2(_endXPos, 0), true);
|
|||
|
|
|
|||
|
|
var baseOffset = vl.FirstDocumentLine.Offset;
|
|||
|
|
var startOffset = baseOffset + vl.GetRelativeOffset(startVc);
|
|||
|
|
var 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));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private TextViewPosition GetStart()
|
|||
|
|
{
|
|||
|
|
var segment = (_startLine < _endLine ? _segments.First() : _segments.Last());
|
|||
|
|
if (_startXPos < _endXPos)
|
|||
|
|
{
|
|||
|
|
return new TextViewPosition(_document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
|
|||
|
|
}
|
|||
|
|
return new TextViewPosition(_document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private TextViewPosition GetEnd()
|
|||
|
|
{
|
|||
|
|
var segment = (_startLine < _endLine ? _segments.Last() : _segments.First());
|
|||
|
|
if (_startXPos < _endXPos)
|
|||
|
|
{
|
|||
|
|
return new TextViewPosition(_document.GetLocation(segment.EndOffset), segment.EndVisualColumn);
|
|||
|
|
}
|
|||
|
|
return new TextViewPosition(_document.GetLocation(segment.StartOffset), segment.StartVisualColumn);
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override string GetText()
|
|||
|
|
{
|
|||
|
|
var b = new StringBuilder();
|
|||
|
|
foreach (var s in Segments)
|
|||
|
|
{
|
|||
|
|
if (b.Length > 0)
|
|||
|
|
b.AppendLine();
|
|||
|
|
b.Append(_document.GetText(s));
|
|||
|
|
}
|
|||
|
|
return b.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition)
|
|||
|
|
{
|
|||
|
|
return SetEndpoint(endPosition);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override int Length
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
return Segments.Sum(s => s.Length);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override bool EnableVirtualSpace => true;
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override ISegment SurroundingSegment => new SimpleSegment(_topLeftOffset, _bottomRightOffset - _topLeftOffset);
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override IEnumerable<SelectionSegment> Segments => _segments;
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override TextViewPosition StartPosition { get; }
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override TextViewPosition EndPosition { get; }
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
public override bool Equals(object obj)
|
|||
|
|
{
|
|||
|
|
var r = obj as RectangleSelection;
|
|||
|
|
// ReSharper disable CompareOfFloatsByEqualityOperator
|
|||
|
|
return r != null && r.TextArea == TextArea
|
|||
|
|
&& r._topLeftOffset == _topLeftOffset && r._bottomRightOffset == _bottomRightOffset
|
|||
|
|
&& r._startLine == _startLine && r._endLine == _endLine
|
|||
|
|
&& r._startXPos == _startXPos && r._endXPos == _endXPos;
|
|||
|
|
// ReSharper restore CompareOfFloatsByEqualityOperator
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
public override int GetHashCode()
|
|||
|
|
{
|
|||
|
|
return _topLeftOffset ^ _bottomRightOffset;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override Selection SetEndpoint(TextViewPosition endPosition)
|
|||
|
|
{
|
|||
|
|
return new RectangleSelection(TextArea, _startLine, _startXPos, endPosition);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private int GetVisualColumnFromXPos(int line, float xPos)
|
|||
|
|
{
|
|||
|
|
var vl = TextArea.TextView.GetOrConstructVisualLine(TextArea.Document.GetLineByNumber(line));
|
|||
|
|
return vl.GetVisualColumn(new Vector2(xPos, 0), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc/>
|
|||
|
|
internal override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e)
|
|||
|
|
{
|
|||
|
|
var newStartLocation = TextArea.Document.GetLocation(e.GetNewOffset(_topLeftOffset, AnchorMovementType.AfterInsertion));
|
|||
|
|
var 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/>
|
|||
|
|
internal override void ReplaceSelectionWithText(string newText)
|
|||
|
|
{
|
|||
|
|
if (newText == null)
|
|||
|
|
throw new ArgumentNullException(nameof(newText));
|
|||
|
|
using (TextArea.Document.RunUpdate())
|
|||
|
|
{
|
|||
|
|
int insertionLength;
|
|||
|
|
var firstInsertionLength = 0;
|
|||
|
|
var editOffset = Math.Min(_topLeftOffset, _bottomRightOffset);
|
|||
|
|
TextViewPosition pos;
|
|||
|
|
if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid)
|
|||
|
|
{
|
|||
|
|
// insert same text into every line
|
|||
|
|
foreach (var lineSegment in Segments.Reverse())
|
|||
|
|
{
|
|||
|
|
ReplaceSingleLineText(TextArea, lineSegment, newText, out insertionLength);
|
|||
|
|
firstInsertionLength = insertionLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pos = new TextViewPosition(_document.GetLocation(editOffset + firstInsertionLength));
|
|||
|
|
|
|||
|
|
TextArea.Selection = new RectangleSelection(TextArea, pos, Math.Max(_startLine, _endLine), GetXPos(TextArea, pos));
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
var lines = newText.Split(NewLineFinder.NewlineStrings, _segments.Count, StringSplitOptions.None);
|
|||
|
|
for (var 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 = new TextViewPosition(Math.Max(_startLine, _endLine), pos.Column);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private 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
|
|||
|
|
{
|
|||
|
|
var segmentsToDelete = textArea.GetDeletableSegments(lineSegment);
|
|||
|
|
for (var 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>
|
|||
|
|
internal static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText)
|
|||
|
|
{
|
|||
|
|
if (textArea == null)
|
|||
|
|
throw new ArgumentNullException(nameof(textArea));
|
|||
|
|
if (text == null)
|
|||
|
|
throw new ArgumentNullException(nameof(text));
|
|||
|
|
var newLineCount = text.AsEnumerable().Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today.
|
|||
|
|
var endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column);
|
|||
|
|
if (endLocation.Line <= textArea.Document.LineCount)
|
|||
|
|
{
|
|||
|
|
var endOffset = textArea.Document.GetOffset(endLocation);
|
|||
|
|
if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation)
|
|||
|
|
{
|
|||
|
|
var rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition));
|
|||
|
|
rsel.ReplaceSelectionWithText(text);
|
|||
|
|
if (selectInsertedText && textArea.Selection is RectangleSelection)
|
|||
|
|
{
|
|||
|
|
var sel = (RectangleSelection)textArea.Selection;
|
|||
|
|
textArea.Selection = new RectangleSelection(textArea, startPosition, sel._endLine, sel._endXPos);
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TODO: clipboard
|
|||
|
|
|
|||
|
|
///// <summary>
|
|||
|
|
///// Gets the name of the entry in the DataObject that signals rectangle selections.
|
|||
|
|
///// </summary>
|
|||
|
|
//public const string RectangularSelectionDataType = "AvalonEditRectangularSelection";
|
|||
|
|
|
|||
|
|
//public override System.Windows.DataObject CreateDataObject(TextArea textArea)
|
|||
|
|
//{
|
|||
|
|
// var data = base.CreateDataObject(textArea);
|
|||
|
|
|
|||
|
|
// if (EditingCommandHandler.ConfirmDataFormat(textArea, data, RectangularSelectionDataType)) {
|
|||
|
|
// 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 $"[RectangleSelection {_startLine} {_topLeftOffset} {_startXPos} to {_endLine} {_bottomRightOffset} {_endXPos}]";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|