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); } }