// 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.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Windows; using System.Windows.Documents; using System.Windows.Input; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Editing { /// /// We re-use the CommandBinding and InputBinding instances between multiple text areas, /// so this class is static. /// static class EditingCommandHandler { /// /// Creates a new for the text area. /// public static TextAreaInputHandler Create(TextArea textArea) { TextAreaInputHandler handler = new TextAreaInputHandler(textArea); handler.CommandBindings.AddRange(CommandBindings); handler.InputBindings.AddRange(InputBindings); return handler; } static readonly List CommandBindings = new List(); static readonly List InputBindings = new List(); static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) { CommandBindings.Add(new CommandBinding(command, handler)); InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key)); } static EditingCommandHandler() { CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, OnDelete(ApplicationCommands.NotACommand), CanDelete)); AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(EditingCommands.SelectRightByCharacter)); AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(EditingCommands.SelectRightByWord)); AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(EditingCommands.SelectLeftByCharacter)); InputBindings.Add(new KeyBinding(EditingCommands.Backspace, Key.Back, ModifierKeys.Shift)); // make Shift-Backspace do the same as plain backspace AddBinding(EditingCommands.DeletePreviousWord, ModifierKeys.Control, Key.Back, OnDelete(EditingCommands.SelectLeftByWord)); AddBinding(EditingCommands.EnterParagraphBreak, ModifierKeys.None, Key.Enter, OnEnter); AddBinding(EditingCommands.EnterLineBreak, ModifierKeys.Shift, Key.Enter, OnEnter); AddBinding(EditingCommands.TabForward, ModifierKeys.None, Key.Tab, OnTab); AddBinding(EditingCommands.TabBackward, ModifierKeys.Shift, Key.Tab, OnShiftTab); CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, CanCutOrCopy)); CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCut, CanCutOrCopy)); CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.DeleteLine, OnDeleteLine)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveLeadingWhitespace, OnRemoveLeadingWhitespace)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveTrailingWhitespace, OnRemoveTrailingWhitespace)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToUppercase, OnConvertToUpperCase)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToLowercase, OnConvertToLowerCase)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToTitleCase, OnConvertToTitleCase)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.InvertCase, OnInvertCase)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertTabsToSpaces, OnConvertTabsToSpaces)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertSpacesToTabs, OnConvertSpacesToTabs)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingTabsToSpaces, OnConvertLeadingTabsToSpaces)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingSpacesToTabs, OnConvertLeadingSpacesToTabs)); CommandBindings.Add(new CommandBinding(AvalonEditCommands.IndentSelection, OnIndentSelection)); } static TextArea GetTextArea(object target) { return target as TextArea; } #region Text Transformation Helpers enum DefaultSegmentType { None, WholeDocument, CurrentLine } /// /// Calls transformLine on all lines in the selected range. /// transformLine needs to handle read-only segments! /// static void TransformSelectedLines(Action transformLine, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { using (textArea.Document.RunUpdate()) { DocumentLine start, end; if (textArea.Selection.IsEmpty) { if (defaultSegmentType == DefaultSegmentType.CurrentLine) { start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line); } else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { start = textArea.Document.Lines.First(); end = textArea.Document.Lines.Last(); } else { start = end = null; } } else { ISegment segment = textArea.Selection.SurroundingSegment; start = textArea.Document.GetLineByOffset(segment.Offset); end = textArea.Document.GetLineByOffset(segment.EndOffset); // don't include the last line if no characters on it are selected if (start != end && end.Offset == segment.EndOffset) end = end.PreviousLine; } if (start != null) { transformLine(textArea, start); while (start != end) { start = start.NextLine; transformLine(textArea, start); } } } textArea.Caret.BringCaretToView(); args.Handled = true; } } /// /// Calls transformLine on all writable segment in the selected range. /// static void TransformSelectedSegments(Action transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { using (textArea.Document.RunUpdate()) { IEnumerable segments; if (textArea.Selection.IsEmpty) { if (defaultSegmentType == DefaultSegmentType.CurrentLine) { segments = new ISegment[] { textArea.Document.GetLineByNumber(textArea.Caret.Line) }; } else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { segments = textArea.Document.Lines.Cast(); } else { segments = null; } } else { segments = textArea.Selection.Segments; } if (segments != null) { foreach (ISegment segment in segments.Reverse()) { foreach (ISegment writableSegment in textArea.GetDeletableSegments(segment).Reverse()) { transformSegment(textArea, writableSegment); } } } } textArea.Caret.BringCaretToView(); args.Handled = true; } } #endregion #region EnterLineBreak static void OnEnter(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.IsKeyboardFocused) { textArea.PerformTextInput("\n"); args.Handled = true; } } #endregion #region Tab static void OnTab(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { using (textArea.Document.RunUpdate()) { if (textArea.Selection.IsMultiline(textArea.Document)) { var segment = textArea.Selection.SurroundingSegment; int oldSelectionStart = segment.Offset; int oldCaretPos = textArea.Caret.Offset; DocumentLine start = textArea.Document.GetLineByOffset(segment.Offset); DocumentLine end = textArea.Document.GetLineByOffset(segment.EndOffset); // don't include the last line if no characters on it are selected if (start != end && end.Offset == segment.EndOffset) end = end.PreviousLine; DocumentLine current = start; while (true) { int offset = current.Offset; if (textArea.ReadOnlySectionProvider.CanInsert(offset)) textArea.Document.Insert(offset, textArea.Options.IndentationString); if (current == end) break; current = current.NextLine; } // Fix for http://community.sharpdevelop.net/forums/t/11909.aspx: // The insertion at the first line will move the start of selection, so we change the selection // to keep the old start offset if it started at the beginning of the line. if (textArea.Selection.Segments.Count() == 1 && oldSelectionStart == start.Offset) { textArea.Selection = new SimpleSelection(oldSelectionStart, textArea.Selection.SurroundingSegment.EndOffset); } // do the same to the caret: if (oldCaretPos == start.Offset) textArea.Caret.Offset = oldCaretPos; } else { string indentationString = textArea.Options.GetIndentationString(textArea.Caret.Column); textArea.ReplaceSelectionWithText(indentationString); } } textArea.Caret.BringCaretToView(); args.Handled = true; } } static void OnShiftTab(object target, ExecutedRoutedEventArgs args) { TransformSelectedLines( delegate (TextArea textArea, DocumentLine line) { int offset = line.Offset; ISegment s = TextUtilities.GetSingleIndentationSegment(textArea.Document, offset, textArea.Options.IndentationSize); if (s.Length > 0) { s = textArea.GetDeletableSegments(s).FirstOrDefault(); if (s != null && s.Length > 0) { textArea.Document.Remove(s.Offset, s.Length); } } }, target, args, DefaultSegmentType.CurrentLine); } #endregion #region Delete static ExecutedRoutedEventHandler OnDelete(RoutedUICommand selectingCommand) { return (target, args) => { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { // call BeginUpdate before running the 'selectingCommand' // so that undoing the delete does not select the deleted character using (textArea.Document.RunUpdate()) { if (textArea.Selection.IsEmpty) { TextViewPosition oldCaretPosition = textArea.Caret.Position; selectingCommand.Execute(args.Parameter, textArea); bool hasSomethingDeletable = false; foreach (ISegment s in textArea.Selection.Segments) { if (textArea.GetDeletableSegments(s).Length > 0) { hasSomethingDeletable = true; break; } } if (!hasSomethingDeletable) { // If nothing in the selection is deletable; then reset caret+selection // to the previous value. This prevents the caret from moving through read-only sections. textArea.Caret.Position = oldCaretPosition; textArea.Selection = Selection.Empty; } } textArea.RemoveSelectedText(); } textArea.Caret.BringCaretToView(); args.Handled = true; } }; } static void CanDelete(object target, CanExecuteRoutedEventArgs args) { // HasSomethingSelected for delete command TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { args.CanExecute = !textArea.Selection.IsEmpty; args.Handled = true; } } #endregion #region Clipboard commands static void CanCutOrCopy(object target, CanExecuteRoutedEventArgs args) { // HasSomethingSelected for copy and cut commands TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { args.CanExecute = textArea.Options.CutCopyWholeLine || !textArea.Selection.IsEmpty; args.Handled = true; } } static void OnCopy(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); CopyWholeLine(textArea, currentLine); } else { CopySelectedText(textArea); } args.Handled = true; } } static void OnCut(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); CopyWholeLine(textArea, currentLine); textArea.Document.Remove(currentLine.Offset, currentLine.TotalLength); } else { CopySelectedText(textArea); textArea.RemoveSelectedText(); } textArea.Caret.BringCaretToView(); args.Handled = true; } } static void CopySelectedText(TextArea textArea) { var data = textArea.Selection.CreateDataObject(textArea); try { Clipboard.SetDataObject(data, true); } catch (ExternalException) { // Apparently this exception sometimes happens randomly. // The MS controls just ignore it, so we'll do the same. return; } string text = textArea.Selection.GetText(textArea.Document); text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); textArea.OnTextCopied(new TextEventArgs(text)); } const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy static void CopyWholeLine(TextArea textArea, DocumentLine line) { ISegment wholeLine = new SimpleSegment(line.Offset, line.TotalLength); string text = textArea.Document.GetText(wholeLine); // Ensure we use the appropriate newline sequence for the OS text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); DataObject data = new DataObject(text); // Also copy text in HTML format to clipboard - good for pasting text into Word // or to the SharpDevelop forums. IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options))); MemoryStream lineSelected = new MemoryStream(1); lineSelected.WriteByte(1); data.SetData(LineSelectedType, lineSelected, false); try { Clipboard.SetDataObject(data, true); } catch (ExternalException) { // Apparently this exception sometimes happens randomly. // The MS controls just ignore it, so we'll do the same. return; } textArea.OnTextCopied(new TextEventArgs(text)); } static void CanPaste(object target, CanExecuteRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { args.CanExecute = textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset) && Clipboard.ContainsText(); args.Handled = true; } } static void OnPaste(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { Debug.WriteLine( Clipboard.GetText(TextDataFormat.Html) ); // convert text back to correct newlines for this document string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); string text = TextUtilities.NormalizeNewLines(Clipboard.GetText(), newLine); if (!string.IsNullOrEmpty(text)) { bool fullLine = textArea.Options.CutCopyWholeLine && Clipboard.ContainsData(LineSelectedType); bool rectangular = Clipboard.ContainsData(RectangleSelection.RectangularSelectionDataType); if (fullLine) { DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); if (textArea.ReadOnlySectionProvider.CanInsert(currentLine.Offset)) { textArea.Document.Insert(currentLine.Offset, text); } } else if (rectangular && textArea.Selection.IsEmpty) { if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Offset, text, false)) textArea.ReplaceSelectionWithText(text); } else { textArea.ReplaceSelectionWithText(text); } } textArea.Caret.BringCaretToView(); args.Handled = true; } } #endregion #region DeleteLine static void OnDeleteLine(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); textArea.Selection = new SimpleSelection(currentLine.Offset, currentLine.Offset + currentLine.TotalLength); textArea.RemoveSelectedText(); args.Handled = true; } } #endregion #region Remove..Whitespace / Convert Tabs-Spaces static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args) { TransformSelectedLines( delegate (TextArea textArea, DocumentLine line) { textArea.Document.Remove(TextUtilities.GetLeadingWhitespace(textArea.Document, line)); }, target, args, DefaultSegmentType.WholeDocument); } static void OnRemoveTrailingWhitespace(object target, ExecutedRoutedEventArgs args) { TransformSelectedLines( delegate (TextArea textArea, DocumentLine line) { textArea.Document.Remove(TextUtilities.GetTrailingWhitespace(textArea.Document, line)); }, target, args, DefaultSegmentType.WholeDocument); } static void OnConvertTabsToSpaces(object target, ExecutedRoutedEventArgs args) { TransformSelectedSegments(ConvertTabsToSpaces, target, args, DefaultSegmentType.WholeDocument); } static void OnConvertLeadingTabsToSpaces(object target, ExecutedRoutedEventArgs args) { TransformSelectedLines( delegate (TextArea textArea, DocumentLine line) { ConvertTabsToSpaces(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); }, target, args, DefaultSegmentType.WholeDocument); } static void ConvertTabsToSpaces(TextArea textArea, ISegment segment) { TextDocument document = textArea.Document; int endOffset = segment.EndOffset; string indentationString = new string(' ', textArea.Options.IndentationSize); for (int offset = segment.Offset; offset < endOffset; offset++) { if (document.GetCharAt(offset) == '\t') { document.Replace(offset, 1, indentationString, OffsetChangeMappingType.CharacterReplace); endOffset += indentationString.Length - 1; } } } static void OnConvertSpacesToTabs(object target, ExecutedRoutedEventArgs args) { TransformSelectedSegments(ConvertSpacesToTabs, target, args, DefaultSegmentType.WholeDocument); } static void OnConvertLeadingSpacesToTabs(object target, ExecutedRoutedEventArgs args) { TransformSelectedLines( delegate (TextArea textArea, DocumentLine line) { ConvertSpacesToTabs(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); }, target, args, DefaultSegmentType.WholeDocument); } static void ConvertSpacesToTabs(TextArea textArea, ISegment segment) { TextDocument document = textArea.Document; int endOffset = segment.EndOffset; int indentationSize = textArea.Options.IndentationSize; int spacesCount = 0; for (int offset = segment.Offset; offset < endOffset; offset++) { if (document.GetCharAt(offset) == ' ') { spacesCount++; if (spacesCount == indentationSize) { document.Replace(offset - (indentationSize - 1), indentationSize, "\t", OffsetChangeMappingType.CharacterReplace); spacesCount = 0; offset -= indentationSize - 1; endOffset -= indentationSize - 1; } } else { spacesCount = 0; } } } #endregion #region Convert...Case static void ConvertCase(Func transformText, object target, ExecutedRoutedEventArgs args) { TransformSelectedSegments( delegate (TextArea textArea, ISegment segment) { string oldText = textArea.Document.GetText(segment); string newText = transformText(oldText); textArea.Document.Replace(segment.Offset, segment.Length, newText, OffsetChangeMappingType.CharacterReplace); }, target, args, DefaultSegmentType.WholeDocument); } static void OnConvertToUpperCase(object target, ExecutedRoutedEventArgs args) { ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToUpper, target, args); } static void OnConvertToLowerCase(object target, ExecutedRoutedEventArgs args) { ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToLower, target, args); } static void OnConvertToTitleCase(object target, ExecutedRoutedEventArgs args) { ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToTitleCase, target, args); } static void OnInvertCase(object target, ExecutedRoutedEventArgs args) { ConvertCase(InvertCase, target, args); } static string InvertCase(string text) { CultureInfo culture = CultureInfo.CurrentCulture; char[] buffer = text.ToCharArray(); for (int i = 0; i < buffer.Length; ++i) { char c = buffer[i]; buffer[i] = char.IsUpper(c) ? char.ToLower(c, culture) : char.ToUpper(c, culture); } return new string(buffer); } #endregion static void OnIndentSelection(object target, ExecutedRoutedEventArgs args) { TextArea textArea = GetTextArea(target); if (textArea != null && textArea.Document != null) { using (textArea.Document.RunUpdate()) { int start, end; if (textArea.Selection.IsEmpty) { start = 1; end = textArea.Document.LineCount; } else { start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset).LineNumber; end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset).LineNumber; } textArea.IndentationStrategy.IndentLines(textArea.Document, start, end); } textArea.Caret.BringCaretToView(); args.Handled = true; } } } }