diff options
| author | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
|---|---|---|
| committer | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
| commit | 080f1697e97e13461ec6df4d31c8924d01257a1b (patch) | |
| tree | b1fe0285de7bc9bc52e9e2195e66fe022bf8f5b3 /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs | |
| parent | 1608e69a417bc5e40a607c3958c4a60f19f66f1a (diff) | |
| download | Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.tar.gz Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.zip | |
MERGE
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs')
| -rw-r--r-- | Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs new file mode 100644 index 000000000..685e7ae31 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs @@ -0,0 +1,578 @@ +// 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 Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Highlighting; +using Tango.Scripting.Editors.Search; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// We re-use the CommandBinding and InputBinding instances between multiple text areas, + /// so this class is static. + /// </summary> + static class EditingCommandHandler + { + /// <summary> + /// Creates a new <see cref="TextAreaInputHandler"/> for the text area. + /// </summary> + public static TextAreaInputHandler Create(TextArea textArea) + { + TextAreaInputHandler handler = new TextAreaInputHandler(textArea); + handler.CommandBindings.AddRange(CommandBindings); + handler.InputBindings.AddRange(InputBindings); + return handler; + } + + static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>(); + static readonly List<InputBinding> InputBindings = new List<InputBinding>(); + + 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(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(EditingCommands.Backspace, ModifierKeys.Shift, Key.Back)); // 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)); + + TextAreaDefaultInputHandler.WorkaroundWPFMemoryLeak(InputBindings); + } + + static TextArea GetTextArea(object target) + { + return target as TextArea; + } + + #region Text Transformation Helpers + enum DefaultSegmentType + { + None, + WholeDocument, + CurrentLine + } + + /// <summary> + /// Calls transformLine on all lines in the selected range. + /// transformLine needs to handle read-only segments! + /// </summary> + static void TransformSelectedLines(Action<TextArea, DocumentLine> 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; + } + } + + /// <summary> + /// Calls transformLine on all writable segment in the selected range. + /// </summary> + static void TransformSelectedSegments(Action<TextArea, ISegment> transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + IEnumerable<ISegment> 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<ISegment>(); + } else { + segments = null; + } + } else { + segments = textArea.Selection.Segments.Cast<ISegment>(); + } + 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) { + var segment = textArea.Selection.SurroundingSegment; + 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.Replace(offset, 0, textArea.Options.IndentationString, OffsetChangeMappingType.KeepAnchorBeforeInsertion); + if (current == end) + break; + current = current.NextLine; + } + } 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; + if (textArea.Caret.IsInVirtualSpace && selectingCommand == EditingCommands.SelectRightByCharacter) + EditingCommands.SelectRightByWord.Execute(args.Parameter, textArea); + else + 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.ClearSelection(); + } + } + 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); + ISegment[] segmentsToDelete = textArea.GetDeletableSegments(new SimpleSegment(currentLine.Offset, currentLine.TotalLength)); + for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { + textArea.Document.Remove(segmentsToDelete[i]); + } + } 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(); + 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(); + // WPF Clipboard.ContainsText() is safe to call without catching ExternalExceptions + // because it doesn't try to lock the clipboard - it just peeks inside with IsClipboardFormatAvailable(). + args.Handled = true; + } + } + + static void OnPaste(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + IDataObject dataObject; + try { + dataObject = Clipboard.GetDataObject(); + } catch (ExternalException) { + return; + } + if (dataObject == null) + return; + Debug.WriteLine( dataObject.GetData(DataFormats.Html) as string ); + + // convert text back to correct newlines for this document + string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); + string text; + try { + text = (string)dataObject.GetData(DataFormats.UnicodeText); + text = TextUtilities.NormalizeNewLines(text, newLine); + } catch (OutOfMemoryException) { + return; + } + + if (!string.IsNullOrEmpty(text)) { + bool fullLine = textArea.Options.CutCopyWholeLine && dataObject.GetDataPresent(LineSelectedType); + bool rectangular = dataObject.GetDataPresent(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 && !(textArea.Selection is RectangleSelection)) { + if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, 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 = Selection.Create(textArea, 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<string, string> 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; + } + } + } +} |
