From 2db5ccb19e9d3a9c912c472deeea1b697ba28814 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 1 Jun 2009 16:16:20 +0000 Subject: [PATCH] Implemented the commands in the 'Edit>Format' menu for AvalonEdit. Allow using 'command' attribute on with custom routed commands defined in AddIns. Implemented offset mapping in AvalonEdit. This allows replacing text in the document without removing all text markers from the replaced region. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4191 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- AddIns/ICSharpCode.SharpDevelop.addin | 61 +--- .../AvalonEdit.AddIn/AvalonEdit.AddIn.addin | 53 ++++ .../AvalonEdit.AddIn/Src/CodeEditor.cs | 13 - .../AvalonEdit.AddIn/Src/CustomCommands.cs | 8 +- .../AvalonEditCommands.cs | 91 ++++++ .../Document/DocumentChangeEventArgs.cs | 47 +++ .../Document/DocumentChangeOperation.cs | 8 +- .../Document/OffsetChangeMap.cs | 177 ++++++++++++ .../Document/TextDocument.cs | 158 +++++++--- .../Document/TextSegmentCollection.cs | 6 +- .../Document/UndoStack.cs | 3 +- .../Editing/EditingCommandHandler.cs | 273 ++++++++++++++++-- .../Editing/TextArea.cs | 2 +- .../ICSharpCode.AvalonEdit.csproj | 2 + .../Indentation/DefaultIndentationStrategy.cs | 4 +- .../Utils/TextUtilities.cs | 73 ++++- .../Src/Commands/NavigationCommands.cs | 1 - .../Project/Src/Editor/DocumentUtilitites.cs | 2 +- .../Project/Src/Gui/WorkbenchSingleton.cs | 1 - .../Core/Project/Src/AddInTree/AddIn/AddIn.cs | 19 +- .../Project/Src/AddInTree/AddIn/Runtime.cs | 4 +- .../Menu/MenuCommand.cs | 2 +- .../Menu/MenuService.cs | 65 +++-- 23 files changed, 883 insertions(+), 190 deletions(-) create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/AvalonEditCommands.cs create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs diff --git a/AddIns/ICSharpCode.SharpDevelop.addin b/AddIns/ICSharpCode.SharpDevelop.addin index ef128c60cf..7708ff33ce 100644 --- a/AddIns/ICSharpCode.SharpDevelop.addin +++ b/AddIns/ICSharpCode.SharpDevelop.addin @@ -48,7 +48,6 @@ - @@ -1645,7 +1644,7 @@ + command = "ICSharpCode.SharpDevelop.SharpDevelopRoutedCommands.SplitView"/> - - - - - - - - - - - - - - - - - - - - - - + + - + - + diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin index 39cd0ef3b1..e60bdc9178 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin @@ -8,6 +8,7 @@ + @@ -20,4 +21,56 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs index f9c699c9f4..4033a47ca5 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs @@ -154,8 +154,6 @@ namespace ICSharpCode.AvalonEdit.AddIn textEditor.TextArea.Caret.PositionChanged += caret_PositionChanged; textEditor.TextArea.DefaultInputHandler.CommandBindings.Add( new CommandBinding(CustomCommands.CtrlSpaceCompletion, OnCodeCompletion)); - textEditor.TextArea.DefaultInputHandler.CommandBindings.Add( - new CommandBinding(CustomCommands.DeleteLine, OnDeleteLine)); textView.BackgroundRenderers.Add(textMarkerService); textView.LineTransformers.Add(textMarkerService); @@ -383,17 +381,6 @@ namespace ICSharpCode.AvalonEdit.AddIn } } - void OnDeleteLine(object sender, ExecutedRoutedEventArgs e) - { - TextEditor textEditor = GetTextEditorFromSender(sender); - e.Handled = true; - using (textEditor.Document.RunUpdate()) { - DocumentLine currentLine = textEditor.Document.GetLineByNumber(textEditor.TextArea.Caret.Line); - textEditor.Select(currentLine.Offset, currentLine.TotalLength); - textEditor.SelectedText = string.Empty; - } - } - CompletionWindow completionWindow; SharpDevelopInsightWindow insightWindow; diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomCommands.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomCommands.cs index 74836cd205..dbccd6756c 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomCommands.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CustomCommands.cs @@ -11,7 +11,7 @@ using System.Windows.Input; namespace ICSharpCode.AvalonEdit.AddIn { /// - /// Custom commands for AvalonEdit. + /// Custom commands for CodeEditor. /// public static class CustomCommands { @@ -20,11 +20,5 @@ namespace ICSharpCode.AvalonEdit.AddIn new InputGestureCollection { new KeyGesture(Key.Space, ModifierKeys.Control) }); - - public static readonly RoutedCommand DeleteLine = new RoutedCommand( - "DeleteLine", typeof(CodeEditor), - new InputGestureCollection { - new KeyGesture(Key.D, ModifierKeys.Control) - }); } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/AvalonEditCommands.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/AvalonEditCommands.cs new file mode 100644 index 0000000000..8127b33315 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/AvalonEditCommands.cs @@ -0,0 +1,91 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Windows.Input; + +namespace ICSharpCode.AvalonEdit +{ + /// + /// Custom commands for AvalonEdit. + /// + public static class AvalonEditCommands + { + /// + /// Deletes the current line. + /// The default shortcut is Ctrl+D. + /// + public static readonly RoutedCommand DeleteLine = new RoutedCommand( + "DeleteLine", typeof(TextEditor), + new InputGestureCollection { + new KeyGesture(Key.D, ModifierKeys.Control) + }); + + /// + /// Removes leading whitespace from the selected lines (or the whole document if the selection is empty). + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static readonly RoutedCommand RemoveLeadingWhitespace = new RoutedCommand("RemoveLeadingWhitespace", typeof(TextEditor)); + + /// + /// Removes trailing whitespace from the selected lines (or the whole document if the selection is empty). + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static readonly RoutedCommand RemoveTrailingWhitespace = new RoutedCommand("RemoveTrailingWhitespace", typeof(TextEditor)); + + /// + /// Converts the selected text to upper case. + /// + public static readonly RoutedCommand ConvertToUppercase = new RoutedCommand("ConvertToUppercase", typeof(TextEditor)); + + /// + /// Converts the selected text to lower case. + /// + public static readonly RoutedCommand ConvertToLowercase = new RoutedCommand("ConvertToLowercase", typeof(TextEditor)); + + /// + /// Converts the selected text to title case. + /// + public static readonly RoutedCommand ConvertToTitleCase = new RoutedCommand("ConvertToTitleCase", typeof(TextEditor)); + + /// + /// Inverts the case of the selected text. + /// + public static readonly RoutedCommand InvertCase = new RoutedCommand("InvertCase", typeof(TextEditor)); + + /// + /// Converts tabs to spaces in the selected text. + /// + public static readonly RoutedCommand ConvertTabsToSpaces = new RoutedCommand("ConvertTabsToSpaces", typeof(TextEditor)); + + /// + /// Converts spaces to tabs in the selected text. + /// + public static readonly RoutedCommand ConvertSpacesToTabs = new RoutedCommand("ConvertSpacesToTabs", typeof(TextEditor)); + + /// + /// Converts leading tabs to spaces in the selected lines (or the whole document if the selection is empty). + /// + public static readonly RoutedCommand ConvertLeadingTabsToSpaces = new RoutedCommand("ConvertLeadingTabsToSpaces", typeof(TextEditor)); + + /// + /// Converts leading spaces to tabs in the selected lines (or the whole document if the selection is empty). + /// + public static readonly RoutedCommand ConvertLeadingSpacesToTabs = new RoutedCommand("ConvertLeadingSpacesToTabs", typeof(TextEditor)); + + /// + /// Runs the IIndentationStrategy on the selected lines (or the whole document if the selection is empty). + /// + public static readonly RoutedCommand IndentSelection = new RoutedCommand( + "IndentSelection", typeof(TextEditor), + new InputGestureCollection { + new KeyGesture(Key.I, ModifierKeys.Control) + }); + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs index 71948d8323..bf13dc20de 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs @@ -37,11 +37,43 @@ namespace ICSharpCode.AvalonEdit.Document get { return InsertedText.Length; } } + volatile OffsetChangeMap offsetChangeMap; + + /// + /// Gets the OffsetChangeMap associated with this document change. + /// + public OffsetChangeMap OffsetChangeMap { + get { + OffsetChangeMap map = offsetChangeMap; + if (map == null) { + // create OffsetChangeMap on demand + map = new OffsetChangeMap(); + if (this.RemovalLength > 0) + map.Add(new OffsetChangeMapEntry(this.Offset, -this.RemovalLength)); + if (this.InsertionLength > 0) + map.Add(new OffsetChangeMapEntry(this.Offset, this.InsertionLength)); + offsetChangeMap = map; + } + return map; + } + } + + /// + /// Gets the OffsetChangeMap, or null if the default offset map (=removal followed by insertion) is being used. + /// + internal OffsetChangeMap OffsetChangeMapOrNull { + get { + return offsetChangeMap; + } + } + /// /// Gets the new offset where the specified offset moves after this document change. /// public int GetNewOffset(int offset, AnchorMovementType movementType) { + if (offsetChangeMap != null) + return offsetChangeMap.GetNewOffset(offset, movementType); if (offset >= this.Offset) { if (offset <= this.Offset + this.RemovalLength) { offset = this.Offset; @@ -58,12 +90,27 @@ namespace ICSharpCode.AvalonEdit.Document /// Creates a new DocumentChangeEventArgs object. /// public DocumentChangeEventArgs(int offset, int removalLength, string insertedText) + : this(offset, removalLength, insertedText, null) + { + } + + /// + /// Creates a new DocumentChangeEventArgs object. + /// + public DocumentChangeEventArgs(int offset, int removalLength, string insertedText, OffsetChangeMap offsetChangeMap) { if (insertedText == null) throw new ArgumentNullException("insertedText"); + this.Offset = offset; this.RemovalLength = removalLength; this.InsertedText = insertedText; + + if (offsetChangeMap != null) { + if (!offsetChangeMap.IsValidForDocumentChange(offset, removalLength, insertedText.Length)) + throw new ArgumentException("OffsetChangeMap is not valid for this document change", "offsetChangeMap"); + this.offsetChangeMap = offsetChangeMap; + } } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs index 8d2051e57a..65544c1b13 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs @@ -18,23 +18,25 @@ namespace ICSharpCode.AvalonEdit.Document int offset; string removedText; string insertedText; + OffsetChangeMap offsetChangeMap; - public DocumentChangeOperation(TextDocument document, int offset, string removedText, string insertedText) + public DocumentChangeOperation(TextDocument document, int offset, string removedText, string insertedText, OffsetChangeMap offsetChangeMap) { this.document = document; this.offset = offset; this.removedText = removedText; this.insertedText = insertedText; + this.offsetChangeMap = offsetChangeMap; } public void Undo() { - document.Replace(offset, insertedText.Length, removedText); + document.Replace(offset, insertedText.Length, removedText, offsetChangeMap != null ? offsetChangeMap.Invert() : null); } public void Redo() { - document.Replace(offset, removedText.Length, insertedText); + document.Replace(offset, removedText.Length, insertedText, offsetChangeMap); } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs new file mode 100644 index 0000000000..46b1def9de --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs @@ -0,0 +1,177 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace ICSharpCode.AvalonEdit.Document +{ + /// + /// Contains predefined offset change mapping types. + /// + public enum OffsetChangeMappingType + { + /// + /// First the old text is removed, then the new text is inserted. + /// + RemoveAndInsert, + /// + /// The text is replaced character-by-character. + /// If the new text is longer than the old text, a single insertion at the end is used to account for the difference. + /// If the new text is shorter than the old text, a single deletion at the end is used to account for the difference. + /// + CharacterReplace + } + + /// + /// Describes a series of offset changes. + /// + [Serializable] + public sealed class OffsetChangeMap : Collection + { + /// + /// Immutable OffsetChangeMap that is empty. + /// + public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Utils.Empty.ReadOnlyCollection); + + /// + /// Creates a new OffsetChangeMap instance. + /// + public OffsetChangeMap() + { + } + + /// + /// Creates a new OffsetChangeMap instance. + /// + public OffsetChangeMap(int capacity) + : base(new List(capacity)) + { + } + + /// + /// Private constructor for immutable 'Empty' instance. + /// + private OffsetChangeMap(IList entries) + : base(entries) + { + } + + /// + /// Gets the new offset where the specified offset moves after this document change. + /// + public int GetNewOffset(int offset, AnchorMovementType movementType) + { + foreach (OffsetChangeMapEntry entry in this) { + offset = entry.GetNewOffset(offset, movementType); + } + return offset; + } + + /// + /// Gets whether this OffsetChangeMap is a valid explanation for the specified document change. + /// + public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength) + { + int endOffset = offset + removalLength; + foreach (OffsetChangeMapEntry entry in this) { + // check that ChangeMapEntry is in valid range for this document change + if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset) + return false; + endOffset += entry.Delta; + } + // check that the total delta matches + return endOffset == offset + insertionLength; + } + + /// + /// Calculates the inverted OffsetChangeMap (used for the undo operation). + /// + public OffsetChangeMap Invert() + { + if (this == Empty) + return this; + OffsetChangeMap newMap = new OffsetChangeMap(this.Count); + for (int i = this.Count - 1; i >= 0; i--) { + OffsetChangeMapEntry entry = this[i]; + newMap.Add(new OffsetChangeMapEntry(entry.Offset, -entry.Delta)); + } + return newMap; + } + } + + /// + /// An entry in the OffsetChangeMap. + /// This represents the offset of a document change (either insertion or removal, not both at once). + /// + [Serializable] + public struct OffsetChangeMapEntry + { + readonly int offset; + readonly int delta; + + /// + /// The offset at which the change occurs. + /// + public int Offset { + get { return offset; } + } + + /// + /// The change delta. If positive, it is equal to InsertionLength; if negative, it is equal to RemovalLength. + /// + public int Delta { + get { return delta; } + } + + /// + /// The number of characters removed. + /// Returns 0 if this entry represents an insertion. + /// + public int RemovalLength { + get { + return delta < 0 ? -delta : 0; + } + } + + /// + /// The number of characters inserted. + /// Returns 0 if this entry represents a removal. + /// + public int InsertionLength { + get { + return delta > 0 ? delta : 0; + } + } + + /// + /// Gets the new offset where the specified offset moves after this document change. + /// + public int GetNewOffset(int oldOffset, AnchorMovementType movementType) + { + if (oldOffset < this.Offset) + return oldOffset; + if (oldOffset > this.Offset + this.RemovalLength) + return oldOffset + this.Delta; + // offset is inside removed region + if (movementType == AnchorMovementType.AfterInsertion) + return this.Offset + this.InsertionLength; + else + return this.Offset; + } + + /// + /// Creates a new OffsetChangeMapEntry instance. + /// + public OffsetChangeMapEntry(int offset, int delta) + { + this.offset = offset; + this.delta = delta; + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs index c307bdd906..c2d24b2ee7 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs @@ -283,23 +283,31 @@ namespace ICSharpCode.AvalonEdit.Document /// /// Inserts text. /// Runtime: - /// for updating the text buffer: m=size of new text, d=distance to last change + /// for updating the text buffer: m=size of new text, d=distance to last change, n=total text length /// usual: O(m+d) /// rare: O(m+n) - /// for updating the document lines: O(m*log n), m=number of changed lines + /// for updating the document lines: O(m*log n), m=number of changed lines, n=total number of lines in document /// public void Insert(int offset, string text) { Replace(offset, 0, text); } + /// + /// Removes text. + /// + public void Remove(ISegment segment) + { + Replace(segment, string.Empty); + } + /// /// Removes text. /// Runtime: - /// for updating the text buffer: d=distance to last change + /// for updating the text buffer: d=distance to last change, n=total text length /// usual: O(d) /// rare: O(n) - /// for updating the document lines: O(m*log n), m=number of changed lines + /// for updating the document lines: O(m*log n), m=number of changed lines, n=total number of lines in document /// public void Remove(int offset, int length) { @@ -315,60 +323,112 @@ namespace ICSharpCode.AvalonEdit.Document { if (segment == null) throw new ArgumentNullException("segment"); - Replace(segment.Offset, segment.Length, text); + Replace(segment.Offset, segment.Length, text, null); } /// /// Replaces text. + /// + public void Replace(int offset, int length, string text) + { + Replace(offset, length, text, null); + } + + /// + /// Replaces text. /// Runtime: - /// for updating the text buffer: m=size of new text, d=distance to last change + /// for updating the text buffer: m=size of new text, d=distance to last change, n=total text length /// usual: O(m+d) /// rare: O(m+n) - /// for updating the document lines: O(m*log n), m=number of changed lines + /// for updating the document lines: O(m*log n), m=number of changed lines, n=total number of lines in document /// - public void Replace(int offset, int length, string text) + /// The starting offset of the text to be replaced. + /// The length of the text to be replaced. + /// The new text. + /// The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text. + /// This affects how the anchors and segments inside the replaced region behave. + public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType) { - if (inDocumentChanging) - throw new InvalidOperationException("Cannot change document within another document change."); + if (text == null) + throw new ArgumentNullException("text"); + switch (offsetChangeMappingType) { + case OffsetChangeMappingType.RemoveAndInsert: + Replace(offset, length, text, null); + break; + case OffsetChangeMappingType.CharacterReplace: + if (text.Length > length) { + OffsetChangeMap map = new OffsetChangeMap(1); + map.Add(new OffsetChangeMapEntry(offset + length, text.Length - length)); + Replace(offset, length, text, map); + } else if (text.Length < length) { + OffsetChangeMap map = new OffsetChangeMap(1); + map.Add(new OffsetChangeMapEntry(offset + length - text.Length, text.Length - length)); + Replace(offset, length, text, map); + } else { + Replace(offset, length, text, OffsetChangeMap.Empty); + } + break; + } + } + + /// + /// Replaces text. + /// Runtime: + /// for updating the text buffer: m=size of new text, d=distance to last change, n=total text length + /// usual: O(m+d) + /// rare: O(m+n) + /// for updating the document lines: O(m*log n), m=number of changed lines, n=total number of lines in document + /// + /// The starting offset of the text to be replaced. + /// The length of the text to be replaced. + /// The new text. + /// The offsetChangeMap determines how offsets inside the old text are mapped to the new text. + /// This affects how the anchors and segments inside the replaced region behave. + /// If you pass null (the default when using one of the other overloads), the offsets are changed as if + /// the old text is removed first and the new text is inserted later (OffsetChangeMappingType.RemoveAndInsert). + /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place. + /// The offsetChangeMap must be a valid 'explanation' for the document change + public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap) + { + if (text == null) + throw new ArgumentNullException("text"); BeginUpdate(); - // protect document change against corruption by other changes inside the event handlers - inDocumentChanging = true; try { - VerifyRange(offset, length); - if (text == null) - throw new ArgumentNullException("text"); - if (length == 0 && text.Length == 0) - return; - - fireTextChanged = true; - - DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, length, text); - - // fire DocumentChanging event - if (Changing != null) - Changing(this, args); - - DelayedEvents delayedEvents = new DelayedEvents(); - - // now do the real work - anchorTree.RemoveText(offset, length, delayedEvents); - ReplaceInternal(offset, length, text); - anchorTree.InsertText(offset, text.Length); - - delayedEvents.RaiseEvents(); - - // fire DocumentChanged event - if (Changed != null) - Changed(this, args); + if (inDocumentChanging) + throw new InvalidOperationException("Cannot change document within another document change."); + // protect document change against corruption by other changes inside the event handlers + inDocumentChanging = true; + try { + // The range verification must wait until after the BeginUpdate() call because the document + // might be modified inside the UpdateStarted event. + VerifyRange(offset, length); + + DoReplace(offset, length, text, offsetChangeMap); + } finally { + inDocumentChanging = false; + } } finally { - inDocumentChanging = false; EndUpdate(); } } - void ReplaceInternal(int offset, int length, string text) + void DoReplace(int offset, int length, string text, OffsetChangeMap offsetChangeMap) { + if (length == 0 && text.Length == 0) + return; + + DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, length, text, offsetChangeMap); + + // fire DocumentChanging event + if (Changing != null) + Changing(this, args); + + fireTextChanged = true; + DelayedEvents delayedEvents = new DelayedEvents(); + + // now update the textBuffer and lineTree if (offset == 0 && length == textBuffer.Length) { + // optimize replacing the whole document textBuffer.Text = text; lineManager.Rebuild(text); } else { @@ -383,6 +443,24 @@ namespace ICSharpCode.AvalonEdit.Document lineTree.CheckProperties(); #endif } + + // update text anchors + if (offsetChangeMap == null) { + anchorTree.RemoveText(offset, length, delayedEvents); + anchorTree.InsertText(offset, text.Length); + } else { + foreach (OffsetChangeMapEntry entry in offsetChangeMap) { + anchorTree.RemoveText(entry.Offset, entry.RemovalLength, delayedEvents); + anchorTree.InsertText(entry.Offset, entry.InsertionLength); + } + } + + // raise delayed events after our data structures are consistent again + delayedEvents.RaiseEvents(); + + // fire DocumentChanged event + if (Changed != null) + Changed(this, args); } #endregion diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs index 5ed52e4aaf..7b71a0c5d2 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs @@ -80,8 +80,10 @@ namespace ICSharpCode.AvalonEdit.Document void OnDocumentChanged(DocumentChangeEventArgs e) { - RemoveText(e.Offset, e.RemovalLength); - InsertText(e.Offset, e.InsertionLength); + foreach (OffsetChangeMapEntry entry in e.OffsetChangeMap) { + RemoveText(entry.Offset, entry.RemovalLength); + InsertText(entry.Offset, entry.InsertionLength); + } } #endregion diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs index a136892d20..ebeab0d8df 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs @@ -275,7 +275,8 @@ namespace ICSharpCode.AvalonEdit.Document document, e.Offset, document.GetText(e.Offset, e.RemovalLength), - e.InsertedText)); + e.InsertedText, + e.OffsetChangeMapOrNull)); } /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs index 101de497cf..c8afb01915 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs @@ -8,8 +8,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Windows; using System.Windows.Documents; using System.Windows.Input; @@ -62,6 +64,20 @@ namespace ICSharpCode.AvalonEdit.Editing 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) @@ -69,6 +85,84 @@ namespace ICSharpCode.AvalonEdit.Editing 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 { + start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset); + end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset); + } + 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.ReadOnlySectionProvider.GetDeletableSegments(segment).Reverse()) { + transformSegment(textArea, writableSegment); + } + } + } + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + #endregion + #region EnterLineBreak static void OnEnter(object target, ExecutedRoutedEventArgs args) { @@ -112,33 +206,17 @@ namespace ICSharpCode.AvalonEdit.Editing static void OnShiftTab(object target, ExecutedRoutedEventArgs args) { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Document != null) { - using (textArea.Document.RunUpdate()) { - DocumentLine start, end; - if (textArea.Selection.IsEmpty) { - start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line); - } else { - start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset); - end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset); - } - while (true) { - int offset = start.Offset; - ISegment s = TextUtilities.GetSingleIndentationSegment(textArea.Document, offset, textArea.Options.IndentationSize); + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + int offset = line.Offset; + ISegment s = TextUtilities.GetSingleIndentationSegment(line.Document, offset, textArea.Options.IndentationSize); if (s.Length > 0) { s = textArea.ReadOnlySectionProvider.GetDeletableSegments(s).FirstOrDefault(); if (s != null && s.Length > 0) { textArea.Document.Remove(s.Offset, s.Length); } } - if (start == end) - break; - start = start.NextLine; - } - } - textArea.Caret.BringCaretToView(); - args.Handled = true; - } + }, target, args, DefaultSegmentType.CurrentLine); } #endregion @@ -288,5 +366,158 @@ namespace ICSharpCode.AvalonEdit.Editing } } #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.Document.Remove(currentLine.Offset, currentLine.TotalLength); + args.Handled = true; + } + } + #endregion + + #region Remove..Whitespace / Convert Tabs-Spaces + static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + line.Document.Remove(TextUtilities.GetLeadingWhitespace(line)); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnRemoveTrailingWhitespace(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + line.Document.Remove(TextUtilities.GetTrailingWhitespace(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(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(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, "\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(start, end); + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs index e47fee8d90..92454f86a2 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/TextArea.cs @@ -346,7 +346,7 @@ namespace ICSharpCode.AvalonEdit.Editing /// /// Code that updates only the caret but not the selection can cause confusion when /// keys like 'Delete' delete the (possibly invisible) selected text and not the - /// text around the caret (where the will jump to). + /// text around the caret. /// /// So we'll ensure that the caret is inside the selection. /// (when the caret is not in the selection, we'll clear the selection) diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index ea1b2745bd..54dec60f07 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -73,6 +73,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -98,6 +99,7 @@ DocumentLine.cs + TextDocument.cs diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Indentation/DefaultIndentationStrategy.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Indentation/DefaultIndentationStrategy.cs index 7d4ce742e3..e14efcd282 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Indentation/DefaultIndentationStrategy.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Indentation/DefaultIndentationStrategy.cs @@ -25,10 +25,10 @@ namespace ICSharpCode.AvalonEdit.Indentation TextDocument document = line.Document; DocumentLine previousLine = line.PreviousLine; if (previousLine != null) { - ISegment indentationSegment = TextUtilities.GetIndentation(document, previousLine.Offset); + ISegment indentationSegment = TextUtilities.GetWhitespaceAfter(document, previousLine.Offset); string indentation = document.GetText(indentationSegment); // copy indentation to line - indentationSegment = TextUtilities.GetIndentation(document, line.Offset); + indentationSegment = TextUtilities.GetWhitespaceAfter(document, line.Offset); document.Replace(indentationSegment, indentation); } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs index 27b8041929..1b626635a8 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs @@ -27,8 +27,10 @@ namespace ICSharpCode.AvalonEdit.Utils "RS", "US" }; + // DEL (ASCII 127) and // the names of the control characters in the C1 block (Unicode 128 to 159) - static readonly string[] c1Table = { + static readonly string[] delAndC1Table = { + "DEL", "PAD", "HOP", "BPH", "NBH", "IND", "NEL", "SSA", "ESA", "HTS", "HTJ", "VTS", "PLD", "PLU", "RI", "SS2", "SS3", "DCS", "PU1", "PU2", "STS", "CCH", "MW", "SPA", "EPA", "SOS", "SGCI", "SCI", "CSI", "ST", "OSC", @@ -44,23 +46,23 @@ namespace ICSharpCode.AvalonEdit.Utils int num = (int)controlCharacter; if (num < c0Table.Length) return c0Table[num]; - else if (num == 127) - return "DEL"; - else if (num >= 128 && num < 128 + c1Table.Length) - return c1Table[num - 128]; + else if (num >= 127 && num <= 159) + return delAndC1Table[num - 127]; else return num.ToString("x4", CultureInfo.InvariantCulture); } #endregion - #region GetIndentation + #region GetWhitespace /// - /// Gets all indentation starting at offset. + /// Gets all whitespace (' ' and '\t', but no newlines) after offset. /// /// The text source. - /// The offset where the indentation starts. - /// The segment containing the indentation. - public static ISegment GetIndentation(ITextSource textSource, int offset) + /// The offset where the whitespace starts. + /// The segment containing the whitespace. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static ISegment GetWhitespaceAfter(ITextSource textSource, int offset) { if (textSource == null) throw new ArgumentNullException("textSource"); @@ -72,6 +74,57 @@ namespace ICSharpCode.AvalonEdit.Utils } return new SimpleSegment(offset, pos - offset); } + + /// + /// Gets all whitespace (' ' and '\t', but no newlines) before offset. + /// + /// The text source. + /// The offset where the whitespace ends. + /// The segment containing the whitespace. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static ISegment GetWhitespaceBefore(ITextSource textSource, int offset) + { + if (textSource == null) + throw new ArgumentNullException("textSource"); + int pos; + for (pos = offset - 1; pos >= 0; pos--) { + char c = textSource.GetCharAt(pos); + if (c != ' ' && c != '\t') + break; + } + return new SimpleSegment(pos, offset - pos); + } + + /// + /// Gets the leading whitespace segment on the document line. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static ISegment GetLeadingWhitespace(DocumentLine documentLine) + { + if (documentLine == null) + throw new ArgumentNullException("documentLine"); + return GetWhitespaceAfter(documentLine.Document, documentLine.Offset); + } + + /// + /// Gets the trailing whitespace segment on the document line. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "WPF uses 'Whitespace'")] + public static ISegment GetTrailingWhitespace(DocumentLine documentLine) + { + if (documentLine == null) + throw new ArgumentNullException("documentLine"); + ISegment segment = GetWhitespaceBefore(documentLine.Document, documentLine.EndOffset); + // If the whole line consists of whitespace, we consider all of it as leading whitespace, + // so return an empty segment as trailing whitespace. + if (segment.Offset == documentLine.Offset) + return new SimpleSegment(documentLine.EndOffset, 0); + else + return segment; + } #endregion #region GetSingleIndentationSegment diff --git a/src/Main/Base/Project/Src/Commands/NavigationCommands.cs b/src/Main/Base/Project/Src/Commands/NavigationCommands.cs index 4d8ad2fd99..62d8f2656d 100644 --- a/src/Main/Base/Project/Src/Commands/NavigationCommands.cs +++ b/src/Main/Base/Project/Src/Commands/NavigationCommands.cs @@ -55,7 +55,6 @@ namespace ICSharpCode.SharpDevelop.Commands public void UpdateEnabledState() { - CommandManager.InvalidateRequerySuggested(); //splitButton.IsEnabled = NavigationService.CanNavigateBack; //splitButton.IsDropDownEnabled = NavigationService.Count>1; } diff --git a/src/Main/Base/Project/Src/Editor/DocumentUtilitites.cs b/src/Main/Base/Project/Src/Editor/DocumentUtilitites.cs index d28e069052..26e4939d52 100644 --- a/src/Main/Base/Project/Src/Editor/DocumentUtilitites.cs +++ b/src/Main/Base/Project/Src/Editor/DocumentUtilitites.cs @@ -103,7 +103,7 @@ namespace ICSharpCode.SharpDevelop.Editor /// The indentation text. public static string GetIndentation(IDocument document, int offset) { - ISegment segment = TextUtilities.GetIndentation(GetTextSource(document), offset); + ISegment segment = TextUtilities.GetWhitespaceAfter(GetTextSource(document), offset); return document.GetText(segment.Offset, segment.Length); } diff --git a/src/Main/Base/Project/Src/Gui/WorkbenchSingleton.cs b/src/Main/Base/Project/Src/Gui/WorkbenchSingleton.cs index 43509aebd5..21d0326dc1 100644 --- a/src/Main/Base/Project/Src/Gui/WorkbenchSingleton.cs +++ b/src/Main/Base/Project/Src/Gui/WorkbenchSingleton.cs @@ -93,7 +93,6 @@ namespace ICSharpCode.SharpDevelop.Gui { WorkbenchSingleton.workbench = workbench; - Core.Presentation.MenuService.RegisterCommandClass(typeof(SharpDevelopRoutedCommands)); DisplayBindingService.InitializeService(); LayoutConfiguration.LoadLayoutConfiguration(); FileService.InitializeService(); diff --git a/src/Main/Core/Project/Src/AddInTree/AddIn/AddIn.cs b/src/Main/Core/Project/Src/AddInTree/AddIn/AddIn.cs index adfd53e913..13d945b49a 100644 --- a/src/Main/Core/Project/Src/AddInTree/AddIn/AddIn.cs +++ b/src/Main/Core/Project/Src/AddInTree/AddIn/AddIn.cs @@ -28,19 +28,28 @@ namespace ICSharpCode.Core static bool hasShownErrorMessage = false; public object CreateObject(string className) + { + Type t = FindType(className); + if (t != null) + return Activator.CreateInstance(t); + else + return null; + } + + public Type FindType(string className) { LoadDependencies(); foreach (Runtime runtime in runtimes) { - object o = runtime.CreateInstance(className); - if (o != null) { - return o; + Type t = runtime.FindType(className); + if (t != null) { + return t; } } if (hasShownErrorMessage) { - LoggingService.Error("Cannot create object: " + className); + LoggingService.Error("Cannot find class: " + className); } else { hasShownErrorMessage = true; - MessageService.ShowError("Cannot create object: " + className + "\nFuture missing objects will not cause an error message."); + MessageService.ShowError("Cannot find class: " + className + "\nFuture missing objects will not cause an error message."); } return null; } diff --git a/src/Main/Core/Project/Src/AddInTree/AddIn/Runtime.cs b/src/Main/Core/Project/Src/AddInTree/AddIn/Runtime.cs index 0f4c5ddff6..0ef5a9a862 100644 --- a/src/Main/Core/Project/Src/AddInTree/AddIn/Runtime.cs +++ b/src/Main/Core/Project/Src/AddInTree/AddIn/Runtime.cs @@ -111,13 +111,13 @@ namespace ICSharpCode.Core } } - public object CreateInstance(string instance) + public Type FindType(string className) { if (IsActive) { Assembly asm = LoadedAssembly; if (asm == null) return null; - return asm.CreateInstance(instance); + return asm.GetType(className); } else { return null; } diff --git a/src/Main/ICSharpCode.Core.Presentation/Menu/MenuCommand.cs b/src/Main/ICSharpCode.Core.Presentation/Menu/MenuCommand.cs index c9514275b0..22df614ff5 100644 --- a/src/Main/ICSharpCode.Core.Presentation/Menu/MenuCommand.cs +++ b/src/Main/ICSharpCode.Core.Presentation/Menu/MenuCommand.cs @@ -22,7 +22,7 @@ namespace ICSharpCode.Core.Presentation { string commandName = codon.Properties["command"]; if (!string.IsNullOrEmpty(commandName)) { - var wpfCommand = MenuService.GetRegisteredCommand(commandName); + var wpfCommand = MenuService.GetRegisteredCommand(codon.AddIn, commandName); if (wpfCommand != null) { return wpfCommand; } else { diff --git a/src/Main/ICSharpCode.Core.Presentation/Menu/MenuService.cs b/src/Main/ICSharpCode.Core.Presentation/Menu/MenuService.cs index 714163dac5..678fe72ca6 100644 --- a/src/Main/ICSharpCode.Core.Presentation/Menu/MenuService.cs +++ b/src/Main/ICSharpCode.Core.Presentation/Menu/MenuService.cs @@ -20,45 +20,64 @@ namespace ICSharpCode.Core.Presentation /// public static class MenuService { - static List commandClasses = new List { - typeof(ApplicationCommands), - typeof(NavigationCommands) - }; + static Dictionary knownCommands = LoadDefaultKnownCommands(); + + static Dictionary LoadDefaultKnownCommands() + { + var knownCommands = new Dictionary(); + foreach (Type t in new Type[] { typeof(ApplicationCommands), typeof(NavigationCommands) }) { + foreach (PropertyInfo p in t.GetProperties()) { + knownCommands.Add(p.Name, (System.Windows.Input.ICommand)p.GetValue(null, null)); + } + } + return knownCommands; + } /// /// Gets a known WPF command. /// + /// The addIn definition that defines the command class. /// The name of the command, e.g. "Copy". /// The WPF ICommand with the given name, or null if thecommand was not found. - public static System.Windows.Input.ICommand GetRegisteredCommand(string commandName) + public static System.Windows.Input.ICommand GetRegisteredCommand(AddIn addIn, string commandName) { + if (addIn == null) + throw new ArgumentNullException("addIn"); if (commandName == null) throw new ArgumentNullException("commandName"); - lock (commandClasses) { - foreach (Type t in commandClasses) { - PropertyInfo p = t.GetProperty(commandName, BindingFlags.Public | BindingFlags.Static); - if (p != null) { - return (System.Windows.Input.ICommand)(p.GetValue(null, null)); - } - FieldInfo f = t.GetField(commandName, BindingFlags.Public | BindingFlags.Static); - if (f != null) { - return (System.Windows.Input.ICommand)(f.GetValue(null)); - } + System.Windows.Input.ICommand command; + lock (knownCommands) { + if (knownCommands.TryGetValue(commandName, out command)) + return command; + } + int pos = commandName.LastIndexOf('.'); + if (pos > 0) { + string className = commandName.Substring(0, pos); + string propertyName = commandName.Substring(pos + 1); + Type classType = addIn.FindType(className); + if (classType != null) { + PropertyInfo p = classType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static); + if (p != null) + return (System.Windows.Input.ICommand)p.GetValue(null, null); + FieldInfo f = classType.GetField(propertyName, BindingFlags.Public | BindingFlags.Static); + if (f != null) + return (System.Windows.Input.ICommand)f.GetValue(null); } - return null; } + return null; } /// - /// + /// Registers a WPF command for use with the <MenuItem command="name"> syntax. /// - public static void RegisterCommandClass(Type commandClass) + public static void RegisterKnownCommand(string name, System.Windows.Input.ICommand command) { - if (commandClass == null) - throw new ArgumentNullException("commandClass"); - lock (commandClasses) { - if (!commandClasses.Contains(commandClass)) - commandClasses.Add(commandClass); + if (name == null) + throw new ArgumentNullException("name"); + if (command == null) + throw new ArgumentNullException("command"); + lock (knownCommands) { + knownCommands.Add(name, command); } }