// 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.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
#if NREFACTORY
using ICSharpCode.NRefactory;
using ICSharpCode.NRefactory.Editor;
#endif
namespace ICSharpCode.AvalonEdit.Editing
{
///
/// Rectangular selection ("box selection").
///
public sealed class RectangleSelection : Selection
{
#region Commands
///
/// Expands the selection left by one character, creating a rectangular selection.
/// Key gesture: Alt+Shift+Left
///
public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter");
///
/// Expands the selection right by one character, creating a rectangular selection.
/// Key gesture: Alt+Shift+Right
///
public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter");
///
/// Expands the selection left by one word, creating a rectangular selection.
/// Key gesture: Ctrl+Alt+Shift+Left
///
public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord");
///
/// Expands the selection left by one word, creating a rectangular selection.
/// Key gesture: Ctrl+Alt+Shift+Right
///
public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord");
///
/// Expands the selection up by one line, creating a rectangular selection.
/// Key gesture: Alt+Shift+Up
///
public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine");
///
/// Expands the selection up by one line, creating a rectangular selection.
/// Key gesture: Alt+Shift+Down
///
public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine");
///
/// Expands the selection to the start of the line, creating a rectangular selection.
/// Key gesture: Alt+Shift+Home
///
public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart");
///
/// Expands the selection to the end of the line, creating a rectangular selection.
/// Key gesture: Alt+Shift+End
///
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 segments = new List();
#region Constructors
///
/// Creates a new rectangular selection.
///
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, pos.IsAtEndOfLine);
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
///
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();
}
///
public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition)
{
return SetEndpoint(endPosition);
}
///
public override int Length {
get {
return this.Segments.Sum(s => s.Length);
}
}
///
public override bool EnableVirtualSpace {
get { return true; }
}
///
public override ISegment SurroundingSegment {
get {
return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset);
}
}
///
public override IEnumerable Segments {
get { return segments; }
}
///
public override TextViewPosition StartPosition {
get { return start; }
}
///
public override TextViewPosition EndPosition {
get { return end; }
}
///
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;
}
///
public override int GetHashCode()
{
return topLeftOffset ^ bottomRightOffset;
}
///
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);
}
///
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)));
}
///
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;
}
///
/// Performs a rectangular paste operation.
///
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;
}
///
/// Gets the name of the entry in the DataObject that signals rectangle selections.
///
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;
}
///
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);
}
}
}