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) {