From 0305ad5834ef5b94becc6ee48f3be7acb159f4ab Mon Sep 17 00:00:00 2001 From: Justin Clareburt Date: Sun, 7 Aug 2022 02:54:33 +0200 Subject: [PATCH] v4.1.0 - Implemented new command: DuplicateLines (#90) Resolves issue #89 * Implemented basic empty command for DuplicateLine * First working implementation of DuplicateLine. * Fixed issue with multi-caret spans. * Wrapped DuplicateLines in a transaction to create a single Undo entry. * Packaged release for v4.1.0 - with DuplicateLines --- HotCommands/Commands/DuplicateSelection.cs | 67 ++++++++++++++++++- HotCommands/Constants.cs | 1 + HotCommands/Hot Commands Shortcuts.vssettings | 3 +- HotCommands/HotCommandsCommandFilter.cs | 8 ++- HotCommands/HotCommandsPackage.vsct | 36 +++++++--- .../HotCommandsTextViewCreationListener.cs | 5 +- HotCommands/Packaging/ReleaseNotes.txt | 1 + HotCommands/source.extension.vsixmanifest | 2 +- README.md | 11 ++- 9 files changed, 113 insertions(+), 21 deletions(-) diff --git a/HotCommands/Commands/DuplicateSelection.cs b/HotCommands/Commands/DuplicateSelection.cs index 98d7591..483328a 100644 --- a/HotCommands/Commands/DuplicateSelection.cs +++ b/HotCommands/Commands/DuplicateSelection.cs @@ -20,6 +20,9 @@ internal sealed class DuplicateSelection private IServiceProvider ServiceProvider => _package; + private ITextBufferUndoManagerProvider UndoProvider; + + public static void Initialize(Package package) { Instance = new DuplicateSelection(package); @@ -31,6 +34,64 @@ private DuplicateSelection(Package package) throw new ArgumentNullException(nameof(package)); _package = package; } + + public static int HandleCommand_DuplicateLine(IWpfTextView textView, IClassifier classifier, + IOleCommandTarget commandTarget, IEditorOperations editorOperations, + ITextBufferUndoManagerProvider undoManagerProvider) + { + // Use cases: + // Single or Multiple carets + // For each caret/selection (in SelectedSpans): + // - No-text selection + // - Single-line selection + // - Multi-line selection + // - Selection that ends on the first char of the line + + // Create a single transaction so the user can Undo all operations in one go. + ITextBufferUndoManager undoManager = undoManagerProvider.GetTextBufferUndoManager(textView.TextBuffer); + ITextUndoTransaction transaction = undoManager.TextBufferUndoHistory.CreateTransaction("Duplicate Lines"); + + List spans = textView.Selection.SelectedSpans.ToList(); + spans.Reverse(); // Hack: Work from the last selection upward, to avoid changing buffer positions with mutli-caret + foreach (SnapshotSpan span in spans) + { + // Select all the text from the start of the first line to the end of the last line + // Find the start of the first line + SnapshotPoint startPoint = new SnapshotPoint(span.Snapshot, span.Start); + SnapshotPoint startOfFirstLine = startPoint.GetContainingLine().Start; + + // Find the end of the last line + SnapshotPoint endPoint = new SnapshotPoint(span.Snapshot, span.End); + SnapshotPoint endOfLastLine = endPoint.GetContainingLine().End; + // Don't include the last line if the end point is at the very beginning! + bool endsAtLineStart = span.Length > 0 && (endPoint.GetContainingLine().Start.Position == endPoint.Position); + if (endsAtLineStart) + { + // Return the text up to the actual endpoint, not the end of its line. + endOfLastLine = endPoint; + // Note: This means that this text contains a CRLF. Account for that later. + } + + // Fetch the text from the start of first to end of last + SnapshotSpan linesToCopy = new SnapshotSpan(startOfFirstLine, endOfLastLine); + string text = linesToCopy.GetText(); + + // Always end with a new line (CR/LF) + if (!endsAtLineStart) + { + text += Environment.NewLine; // Note: This does not detect the line endings of the current file. + } + + // Insert the text at the start of the first line + textView.TextBuffer.Insert(startOfFirstLine.Position, text); + } + + // Complete the transaction + transaction.Complete(); + + return VSConstants.S_OK; + } + // Helped by source of Microsoft.VisualStudio.Text.Editor.DragDrop.DropHandlerBase.cs in assembly Microsoft.VisualStudio.Text.UI.Wpf, Version=14.0.0.0 public static int HandleCommand(IWpfTextView textView, IClassifier classifier, IOleCommandTarget commandTarget, IEditorOperations editorOperations, bool shiftPressed = false) { @@ -127,10 +188,10 @@ public static int HandleCommand(IWpfTextView textView, IClassifier classifier, I if (isReversed) list.Reverse(); foreach (var trackingSpan in list) { - var span = trackingSpan.GetSpan(textSnapshot); + SnapshotSpan span = trackingSpan.GetSpan(textSnapshot); text = trackingSpan.GetText(textSnapshot); - var offset = 0; - var insertionPoint = !isReversed ? trackingSpan.GetEndPoint(span.Snapshot) : trackingSpan.GetStartPoint(span.Snapshot); + int offset = 0; + SnapshotPoint insertionPoint = !isReversed ? trackingSpan.GetEndPoint(span.Snapshot) : trackingSpan.GetStartPoint(span.Snapshot); var virtualBufferPosition = new VirtualSnapshotPoint(insertionPoint); virtualBufferPosition = isReversed && !shiftPressed ? new VirtualSnapshotPoint(insertionPoint.Add(text.Length)) : !isReversed && shiftPressed ? new VirtualSnapshotPoint(insertionPoint.Add(-text.Length)) : virtualBufferPosition; diff --git a/HotCommands/Constants.cs b/HotCommands/Constants.cs index 68d2dac..bcd1f2b 100644 --- a/HotCommands/Constants.cs +++ b/HotCommands/Constants.cs @@ -5,6 +5,7 @@ namespace HotCommands public class Constants { public static readonly Guid HotCommandsGuid = new Guid("1023dc3d-550c-46b8-a3ec-c6b03431642c"); + public const uint DuplicateLineCmdId = 0x1018; public const uint DuplicateSelectionCmdId = 0x1019; public const uint DuplicateSelectionReverseCmdId = 0x1020; public const uint ToggleCommentCmdId = 0x1021; diff --git a/HotCommands/Hot Commands Shortcuts.vssettings b/HotCommands/Hot Commands Shortcuts.vssettings index 242f2b8..f28b1a2 100644 --- a/HotCommands/Hot Commands Shortcuts.vssettings +++ b/HotCommands/Hot Commands Shortcuts.vssettings @@ -18,8 +18,9 @@ Ctrl+Shift+Bkspce Ctrl+D + Ctrl+Shift+D Ctrl+D - Ctrl+Shift+D + Ctrl+W Ctrl+Shift+W diff --git a/HotCommands/HotCommandsCommandFilter.cs b/HotCommands/HotCommandsCommandFilter.cs index 6456cdf..dda3bd7 100644 --- a/HotCommands/HotCommandsCommandFilter.cs +++ b/HotCommands/HotCommandsCommandFilter.cs @@ -17,17 +17,20 @@ internal sealed class HotCommandsCommandFilter : IOleCommandTarget private readonly IClassifier classifier; private readonly SVsServiceProvider globalServiceProvider; private IEditorOperations editorOperations; + private readonly ITextBufferUndoManagerProvider undoManagerProvider; private IVsStatusbar statusBarService; internal IVsStatusbar StatusBarService => this.statusBarService ?? (this.statusBarService = globalServiceProvider.GetService(typeof(SVsStatusbar)) as IVsStatusbar); public HotCommandsCommandFilter(IWpfTextView textView, IClassifierAggregatorService aggregatorFactory, - SVsServiceProvider globalServiceProvider, IEditorOperationsFactoryService editorOperationsFactory) + SVsServiceProvider globalServiceProvider, IEditorOperationsFactoryService editorOperationsFactory, + ITextBufferUndoManagerProvider undoProvider) { this.textView = textView; classifier = aggregatorFactory.GetClassifier(textView.TextBuffer); this.globalServiceProvider = globalServiceProvider; editorOperations = editorOperationsFactory.GetEditorOperations(textView); + this.undoManagerProvider = undoProvider; } public IOleCommandTarget Next { get; internal set; } @@ -52,6 +55,8 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv return ExpandSelection.Instance.HandleCommand(textView, false); case Constants.FormatCodeCmdId: return FormatCode.Instance.HandleCommand(textView, GetShellCommandDispatcher()); + case Constants.DuplicateLineCmdId: + return DuplicateSelection.HandleCommand_DuplicateLine(textView, classifier, GetShellCommandDispatcher(), editorOperations, undoManagerProvider); case Constants.DuplicateSelectionCmdId: return DuplicateSelection.HandleCommand(textView, classifier, GetShellCommandDispatcher(), editorOperations); case Constants.DuplicateSelectionReverseCmdId: @@ -98,6 +103,7 @@ public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, Int case Constants.ToggleCommentCmdId: case Constants.ExpandSelectionCmdId: case Constants.FormatCodeCmdId: + case Constants.DuplicateLineCmdId: case Constants.DuplicateSelectionCmdId: case Constants.DuplicateSelectionReverseCmdId: case Constants.MoveMemberUpCmdId: diff --git a/HotCommands/HotCommandsPackage.vsct b/HotCommands/HotCommandsPackage.vsct index f4ef3b2..acd9494 100644 --- a/HotCommands/HotCommandsPackage.vsct +++ b/HotCommands/HotCommandsPackage.vsct @@ -69,6 +69,15 @@ + @@ -166,31 +175,34 @@ - + - + - + - + - + - + - + - + - + + + + @@ -210,6 +222,7 @@ + @@ -229,7 +242,7 @@ - + @@ -243,6 +256,7 @@ + diff --git a/HotCommands/Listeners/HotCommandsTextViewCreationListener.cs b/HotCommands/Listeners/HotCommandsTextViewCreationListener.cs index 0b7c709..b0a93ce 100644 --- a/HotCommands/Listeners/HotCommandsTextViewCreationListener.cs +++ b/HotCommands/Listeners/HotCommandsTextViewCreationListener.cs @@ -28,11 +28,14 @@ internal sealed class HotCommandsTextViewCreationListener : IVsTextViewCreationL [Import(typeof(IEditorOperationsFactoryService))] private IEditorOperationsFactoryService _editorOperationsFactory; + [Import] + private ITextBufferUndoManagerProvider _undoProvider; + public void VsTextViewCreated(IVsTextView textViewAdapter) { IWpfTextView textView = EditorAdaptersFactoryService.GetWpfTextView(textViewAdapter); - HotCommandsCommandFilter commandFilter = new HotCommandsCommandFilter(textView, _aggregatorFactory, _globalServiceProvider, _editorOperationsFactory); + HotCommandsCommandFilter commandFilter = new HotCommandsCommandFilter(textView, _aggregatorFactory, _globalServiceProvider, _editorOperationsFactory, _undoProvider); IOleCommandTarget next; textViewAdapter.AddCommandFilter(commandFilter, out next); diff --git a/HotCommands/Packaging/ReleaseNotes.txt b/HotCommands/Packaging/ReleaseNotes.txt index 1a3882b..02797a5 100644 --- a/HotCommands/Packaging/ReleaseNotes.txt +++ b/HotCommands/Packaging/ReleaseNotes.txt @@ -1,4 +1,5 @@ Hot Commands for Visual Studio +07-Aug-2022: v4.1.0 Added command: DuplicateLine (Issue #89 - Fabio Marmitt) 10-Jan-2022: v4.0.0 Upgraded to VS2022. Now only targeting VS2022+. 04-May-2020: v3.0.0 Now only targeting VS2017+. Handles Async loading (Issue #81/PR #82) 03-May-2020: v2.1.7 Updated build files so it builds in VS2015, VS2017 and VS2019 diff --git a/HotCommands/source.extension.vsixmanifest b/HotCommands/source.extension.vsixmanifest index 6b3c46d..a1fd51e 100644 --- a/HotCommands/source.extension.vsixmanifest +++ b/HotCommands/source.extension.vsixmanifest @@ -1,7 +1,7 @@ - + Hot Commands A collection of commands and shortcuts for enhanced productivity in Visual Studio IDE. VS2022+ https://marketplace.visualstudio.com/items?itemName=JustinClareburtMSFT.HotCommandsforVisualStudio diff --git a/README.md b/README.md index 1b1d7b0..164b031 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,14 @@ Project for creating new commands and shortcuts for Visual Studio. Ctrl+/ -Duplicate Code /
Duplicate Reversed -Duplicates the currently selected text, or the current line if no selection.
Reversed: Same as Duplicate Code, but places the new code before the current selection (or line). -Ctrl+D /
Ctrl+Shift+D +Duplicate Selection +Duplicates the currently selected text, or the current line if no selection. +Ctrl+D + + +Duplicate Lines +Duplicates the entire line(s) of the current selection, or the current line if no selection. +Ctrl+Shift+D Edit.JoinLines