diff --git a/ICSharpCode.AvalonEdit/AvalonEditCommands.cs b/ICSharpCode.AvalonEdit/AvalonEditCommands.cs
index f37b05a3..91f73e12 100644
--- a/ICSharpCode.AvalonEdit/AvalonEditCommands.cs
+++ b/ICSharpCode.AvalonEdit/AvalonEditCommands.cs
@@ -45,6 +45,26 @@ public static class AvalonEditCommands
new KeyGesture(Key.D, ModifierKeys.Control)
});
+ ///
+ /// Swap the current line and the previous line.
+ /// The default shortcut is Alt+Up.
+ ///
+ public static readonly RoutedCommand SwapLinesUp = new RoutedCommand(
+ "SwapLinesUp", typeof(TextEditor),
+ new InputGestureCollection {
+ new KeyGesture(Key.Up, ModifierKeys.Alt)
+ });
+
+ ///
+ /// Swap the current line and the next line.
+ /// The default shortcut is Alt+Down.
+ ///
+ public static readonly RoutedCommand SwapLinesDown = new RoutedCommand(
+ "SwapLinesDown", typeof(TextEditor),
+ new InputGestureCollection {
+ new KeyGesture(Key.Down, ModifierKeys.Alt)
+ });
+
///
/// Removes leading whitespace from the selected lines (or the whole document if the selection is empty).
///
diff --git a/ICSharpCode.AvalonEdit/Editing/CaretSelectionPreserver.cs b/ICSharpCode.AvalonEdit/Editing/CaretSelectionPreserver.cs
new file mode 100644
index 00000000..f78807e3
--- /dev/null
+++ b/ICSharpCode.AvalonEdit/Editing/CaretSelectionPreserver.cs
@@ -0,0 +1,121 @@
+// 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 ICSharpCode.AvalonEdit.Document;
+
+namespace ICSharpCode.AvalonEdit.Editing
+{
+ ///
+ /// Base class used by the CaretPreserver and SelectionPreserver
+ ///
+ abstract class CaretSelectionPreserver
+ : IDisposable
+ {
+ protected CaretSelectionPreserver(TextArea _textArea)
+ {
+ isDisposed = false;
+ textArea = _textArea;
+ }
+
+ public static CaretSelectionPreserver Create(TextArea textArea)
+ {
+ if (textArea.Selection.IsEmpty) {
+ return new CaretPreserver(textArea);
+ } else {
+ return new SelectionPreserver(textArea);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (isDisposed) return;
+ isDisposed = true;
+ Restore();
+ }
+
+ public abstract void Restore();
+ public abstract void MoveLine(int i);
+
+ private bool isDisposed;
+
+ private TextArea textArea;
+ public TextArea TextArea { get { return textArea; } }
+ }
+
+ ///
+ /// This class moves the current caret position when a line is moved up or down
+ ///
+ class CaretPreserver
+ : CaretSelectionPreserver
+ {
+ public CaretPreserver(TextArea textArea)
+ : base(textArea)
+ {
+ caretLocation = textArea.Caret.Location;
+ }
+
+ public override void Restore()
+ {
+ TextArea.Caret.Location = caretLocation;
+ }
+
+ public override void MoveLine(int i)
+ {
+ caretLocation = new TextLocation(caretLocation.Line + i, caretLocation.Column);
+ }
+
+ TextLocation caretLocation;
+ }
+
+ ///
+ /// This class moves the current selection when the lines containing that selection
+ /// are moved up or down.
+ ///
+ /// NOTE: The SelectionPreserver must inherit from the CaretPreserver as if the caret
+ /// is not within the selection then the selection is not considered valid and is
+ /// cleared. By inheriting from the CaretPreserver we move both the selection and
+ /// the caret at the same time.
+ ///
+ class SelectionPreserver
+ : CaretPreserver
+ {
+ public SelectionPreserver(TextArea textArea)
+ : base(textArea)
+ {
+ startLocation = textArea.Selection.StartPosition.Location;
+ endLocation = textArea.Selection.EndPosition.Location;
+ }
+
+ public override void Restore()
+ {
+ base.Restore();
+ TextArea.Selection = Selection.Create(TextArea, new TextViewPosition(startLocation), new TextViewPosition(endLocation));
+ }
+
+
+ public override void MoveLine(int i)
+ {
+ base.MoveLine(i);
+ startLocation = new TextLocation(startLocation.Line + i, startLocation.Column);
+ endLocation = new TextLocation(endLocation.Line + i, endLocation.Column);
+ }
+
+ TextLocation startLocation, endLocation;
+ }
+}
diff --git a/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs
index fcdda52c..91ae00b7 100644
--- a/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs
+++ b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs
@@ -78,6 +78,9 @@ static EditingCommandHandler()
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ToggleOverstrike, OnToggleOverstrike));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.DeleteLine, OnDeleteLine));
+ CommandBindings.Add(new CommandBinding(AvalonEditCommands.SwapLinesUp, OnSwapLinesUp));
+ CommandBindings.Add(new CommandBinding(AvalonEditCommands.SwapLinesDown, OnSwapLinesDown));
+
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveLeadingWhitespace, OnRemoveLeadingWhitespace));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveTrailingWhitespace, OnRemoveTrailingWhitespace));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToUppercase, OnConvertToUpperCase));
@@ -521,6 +524,94 @@ static void OnDeleteLine(object target, ExecutedRoutedEventArgs args)
}
#endregion
+ #region SwapLines
+ static void OnSwapLinesUp(object target, ExecutedRoutedEventArgs args)
+ {
+ TextArea textArea = GetTextArea(target);
+ if (textArea != null && textArea.Document != null) {
+ using (var caretSelectionPreserver = CaretSelectionPreserver.Create(textArea)) {
+ var selectionFirstLine = textArea.Selection.IsEmpty ? textArea.Caret.Line : textArea.Selection.StartPosition.Line;
+ var selectionLastLine = textArea.Selection.IsEmpty ? textArea.Caret.Line : textArea.Selection.EndPosition.Line;
+ // we cannot move up if the selection is on the first line
+ if (selectionFirstLine == 1) return;
+
+ DocumentLine movedLine = textArea.Document.GetLineByNumber(selectionFirstLine - 1);
+ DocumentLine lastLine = textArea.Document.GetLineByNumber(selectionLastLine);
+
+ using (textArea.Document.RunUpdate()) {
+ textArea.Selection = Selection.Create(textArea, movedLine.Offset, movedLine.Offset + movedLine.TotalLength);
+ string movedLineText = textArea.Selection.GetText();
+ if (lastLine.NextLine == null) {
+ movedLineText = movedLineText.RotateTailLinebreak();
+ }
+ textArea.Document.Insert(lastLine.Offset + lastLine.TotalLength, movedLineText);
+ textArea.RemoveSelectedText();
+ caretSelectionPreserver.MoveLine(-1);
+ }
+ }
+ args.Handled = true;
+ }
+ }
+
+ static void OnSwapLinesDown(object target, ExecutedRoutedEventArgs args)
+ {
+ TextArea textArea = GetTextArea(target);
+ if (textArea != null && textArea.Document != null) {
+ using (var caretSelectionPreserver = CaretSelectionPreserver.Create(textArea)) {
+ var selectionFirstLine = textArea.Selection.IsEmpty ? textArea.Caret.Line : textArea.Selection.StartPosition.Line;
+ var selectionLastLine = textArea.Selection.IsEmpty ? textArea.Caret.Line : textArea.Selection.EndPosition.Line;
+ // we cannot move down if the selection is on the last line
+ if (selectionLastLine == textArea.Document.LineCount) return;
+
+ DocumentLine firstLine = textArea.Document.GetLineByNumber(selectionFirstLine);
+ DocumentLine lastLine = textArea.Document.GetLineByNumber(selectionLastLine);
+ DocumentLine movedLine = textArea.Document.GetLineByNumber(selectionLastLine + 1);
+
+ using (textArea.Document.RunUpdate()) {
+ textArea.Selection = Selection.Create(textArea, lastLine.EndOffset, movedLine.EndOffset);
+ string movedLineText = textArea.Selection.GetText().RotateHeadLinebreak();
+ textArea.Document.Insert(firstLine.Offset, movedLineText);
+ textArea.RemoveSelectedText();
+ caretSelectionPreserver.MoveLine(+1);
+ }
+ }
+ args.Handled = true;
+ }
+ }
+
+ ///
+ /// Move the leading linebreak to the end.
+ ///
+ /// A string which starts with one or more linebreaks.
+ ///
+ private static string RotateHeadLinebreak(this string self)
+ {
+ int len = self.Length;
+ // check if the line break is /n or /r/n
+ string linebreak =
+ (len >= 2 && self.Substring(0, 2) == "\r\n")
+ ? "\r\n"
+ : self.Substring(0, 1);
+ return self.Remove(0, linebreak.Length) + linebreak;
+ }
+
+ ///
+ /// Move the tailing linebreak to the head.
+ ///
+ /// A string which ends with one or more linebreaks.
+ ///
+ private static string RotateTailLinebreak(this string self)
+ {
+ int len = self.Length;
+ // check if the line break is /n or /r/n
+ string linebreak =
+ (len >= 2 && self.Substring(len - 2, 2) == "\r\n")
+ ? "\r\n"
+ : self.Substring(len - 1, 1);
+ return linebreak + self.Remove(len - linebreak.Length);
+ }
+ #endregion
+
#region Remove..Whitespace / Convert Tabs-Spaces
static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args)
{