Skip to content

Commit 0305ad5

Browse files
authored
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
1 parent c34d9ec commit 0305ad5

9 files changed

Lines changed: 113 additions & 21 deletions

HotCommands/Commands/DuplicateSelection.cs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ internal sealed class DuplicateSelection
2020

2121
private IServiceProvider ServiceProvider => _package;
2222

23+
private ITextBufferUndoManagerProvider UndoProvider;
24+
25+
2326
public static void Initialize(Package package)
2427
{
2528
Instance = new DuplicateSelection(package);
@@ -31,6 +34,64 @@ private DuplicateSelection(Package package)
3134
throw new ArgumentNullException(nameof(package));
3235
_package = package;
3336
}
37+
38+
public static int HandleCommand_DuplicateLine(IWpfTextView textView, IClassifier classifier,
39+
IOleCommandTarget commandTarget, IEditorOperations editorOperations,
40+
ITextBufferUndoManagerProvider undoManagerProvider)
41+
{
42+
// Use cases:
43+
// Single or Multiple carets
44+
// For each caret/selection (in SelectedSpans):
45+
// - No-text selection
46+
// - Single-line selection
47+
// - Multi-line selection
48+
// - Selection that ends on the first char of the line
49+
50+
// Create a single transaction so the user can Undo all operations in one go.
51+
ITextBufferUndoManager undoManager = undoManagerProvider.GetTextBufferUndoManager(textView.TextBuffer);
52+
ITextUndoTransaction transaction = undoManager.TextBufferUndoHistory.CreateTransaction("Duplicate Lines");
53+
54+
List<SnapshotSpan> spans = textView.Selection.SelectedSpans.ToList();
55+
spans.Reverse(); // Hack: Work from the last selection upward, to avoid changing buffer positions with mutli-caret
56+
foreach (SnapshotSpan span in spans)
57+
{
58+
// Select all the text from the start of the first line to the end of the last line
59+
// Find the start of the first line
60+
SnapshotPoint startPoint = new SnapshotPoint(span.Snapshot, span.Start);
61+
SnapshotPoint startOfFirstLine = startPoint.GetContainingLine().Start;
62+
63+
// Find the end of the last line
64+
SnapshotPoint endPoint = new SnapshotPoint(span.Snapshot, span.End);
65+
SnapshotPoint endOfLastLine = endPoint.GetContainingLine().End;
66+
// Don't include the last line if the end point is at the very beginning!
67+
bool endsAtLineStart = span.Length > 0 && (endPoint.GetContainingLine().Start.Position == endPoint.Position);
68+
if (endsAtLineStart)
69+
{
70+
// Return the text up to the actual endpoint, not the end of its line.
71+
endOfLastLine = endPoint;
72+
// Note: This means that this text contains a CRLF. Account for that later.
73+
}
74+
75+
// Fetch the text from the start of first to end of last
76+
SnapshotSpan linesToCopy = new SnapshotSpan(startOfFirstLine, endOfLastLine);
77+
string text = linesToCopy.GetText();
78+
79+
// Always end with a new line (CR/LF)
80+
if (!endsAtLineStart)
81+
{
82+
text += Environment.NewLine; // Note: This does not detect the line endings of the current file.
83+
}
84+
85+
// Insert the text at the start of the first line
86+
textView.TextBuffer.Insert(startOfFirstLine.Position, text);
87+
}
88+
89+
// Complete the transaction
90+
transaction.Complete();
91+
92+
return VSConstants.S_OK;
93+
}
94+
3495
// Helped by source of Microsoft.VisualStudio.Text.Editor.DragDrop.DropHandlerBase.cs in assembly Microsoft.VisualStudio.Text.UI.Wpf, Version=14.0.0.0
3596
public static int HandleCommand(IWpfTextView textView, IClassifier classifier, IOleCommandTarget commandTarget, IEditorOperations editorOperations, bool shiftPressed = false)
3697
{
@@ -127,10 +188,10 @@ public static int HandleCommand(IWpfTextView textView, IClassifier classifier, I
127188
if (isReversed) list.Reverse();
128189
foreach (var trackingSpan in list)
129190
{
130-
var span = trackingSpan.GetSpan(textSnapshot);
191+
SnapshotSpan span = trackingSpan.GetSpan(textSnapshot);
131192
text = trackingSpan.GetText(textSnapshot);
132-
var offset = 0;
133-
var insertionPoint = !isReversed ? trackingSpan.GetEndPoint(span.Snapshot) : trackingSpan.GetStartPoint(span.Snapshot);
193+
int offset = 0;
194+
SnapshotPoint insertionPoint = !isReversed ? trackingSpan.GetEndPoint(span.Snapshot) : trackingSpan.GetStartPoint(span.Snapshot);
134195
var virtualBufferPosition = new VirtualSnapshotPoint(insertionPoint);
135196
virtualBufferPosition = isReversed && !shiftPressed ? new VirtualSnapshotPoint(insertionPoint.Add(text.Length))
136197
: !isReversed && shiftPressed ? new VirtualSnapshotPoint(insertionPoint.Add(-text.Length)) : virtualBufferPosition;

HotCommands/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace HotCommands
55
public class Constants
66
{
77
public static readonly Guid HotCommandsGuid = new Guid("1023dc3d-550c-46b8-a3ec-c6b03431642c");
8+
public const uint DuplicateLineCmdId = 0x1018;
89
public const uint DuplicateSelectionCmdId = 0x1019;
910
public const uint DuplicateSelectionReverseCmdId = 0x1020;
1011
public const uint ToggleCommentCmdId = 0x1021;

HotCommands/Hot Commands Shortcuts.vssettings

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
<Shortcut Command="Edit.GoToLastEditLocation" Scope="Global">Ctrl+Shift+Bkspce</Shortcut> <!-- In VS 15.8+ -->
1919
<!--Duplicate Code-->
2020
<Shortcut Command="Edit.Duplicate" Scope="Text Editor">Ctrl+D</Shortcut> <!-- v15.8+ -->
21+
<Shortcut Command="Edit.DuplicateLines" Scope="Text Editor">Ctrl+Shift+D</Shortcut>
2122
<Shortcut Command="Edit.DuplicateSelection" Scope="Text Editor">Ctrl+D</Shortcut>
22-
<Shortcut Command="Edit.DuplicateSelectionReversed" Scope="Text Editor">Ctrl+Shift+D</Shortcut>
23+
<!--<Shortcut Command="Edit.DuplicateSelectionReversed" Scope="Text Editor">Ctrl+Shift+D</Shortcut>-->
2324
<!--Expand Selection-->
2425
<Shortcut Command="Edit.IncreaseSelection" Scope="Text Editor">Ctrl+W</Shortcut> <!-- ExpandSelection in v15.7+ -->
2526
<Shortcut Command="Edit.DecreaseSelection" Scope="Text Editor">Ctrl+Shift+W</Shortcut>

HotCommands/HotCommandsCommandFilter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ internal sealed class HotCommandsCommandFilter : IOleCommandTarget
1717
private readonly IClassifier classifier;
1818
private readonly SVsServiceProvider globalServiceProvider;
1919
private IEditorOperations editorOperations;
20+
private readonly ITextBufferUndoManagerProvider undoManagerProvider;
2021
private IVsStatusbar statusBarService;
2122
internal IVsStatusbar StatusBarService => this.statusBarService
2223
?? (this.statusBarService = globalServiceProvider.GetService(typeof(SVsStatusbar)) as IVsStatusbar);
2324

2425
public HotCommandsCommandFilter(IWpfTextView textView, IClassifierAggregatorService aggregatorFactory,
25-
SVsServiceProvider globalServiceProvider, IEditorOperationsFactoryService editorOperationsFactory)
26+
SVsServiceProvider globalServiceProvider, IEditorOperationsFactoryService editorOperationsFactory,
27+
ITextBufferUndoManagerProvider undoProvider)
2628
{
2729
this.textView = textView;
2830
classifier = aggregatorFactory.GetClassifier(textView.TextBuffer);
2931
this.globalServiceProvider = globalServiceProvider;
3032
editorOperations = editorOperationsFactory.GetEditorOperations(textView);
33+
this.undoManagerProvider = undoProvider;
3134
}
3235

3336
public IOleCommandTarget Next { get; internal set; }
@@ -52,6 +55,8 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv
5255
return ExpandSelection.Instance.HandleCommand(textView, false);
5356
case Constants.FormatCodeCmdId:
5457
return FormatCode.Instance.HandleCommand(textView, GetShellCommandDispatcher());
58+
case Constants.DuplicateLineCmdId:
59+
return DuplicateSelection.HandleCommand_DuplicateLine(textView, classifier, GetShellCommandDispatcher(), editorOperations, undoManagerProvider);
5560
case Constants.DuplicateSelectionCmdId:
5661
return DuplicateSelection.HandleCommand(textView, classifier, GetShellCommandDispatcher(), editorOperations);
5762
case Constants.DuplicateSelectionReverseCmdId:
@@ -98,6 +103,7 @@ public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, Int
98103
case Constants.ToggleCommentCmdId:
99104
case Constants.ExpandSelectionCmdId:
100105
case Constants.FormatCodeCmdId:
106+
case Constants.DuplicateLineCmdId:
101107
case Constants.DuplicateSelectionCmdId:
102108
case Constants.DuplicateSelectionReverseCmdId:
103109
case Constants.MoveMemberUpCmdId:

HotCommands/HotCommandsPackage.vsct

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@
6969
</Strings>
7070
</Button>
7171

72+
<Button guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" priority="0x0000" type="Button">
73+
<CommandFlag>DynamicVisibility</CommandFlag>
74+
<CommandFlag>DefaultInvisible</CommandFlag>
75+
<Strings>
76+
<ButtonText>Duplicate Line</ButtonText>
77+
<MenuText>Duplicate &amp;Line</MenuText>
78+
<ToolTipText>Duplicates the selected text.</ToolTipText>
79+
</Strings>
80+
</Button>
7281
<Button guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x0000" type="Button">
7382
<CommandFlag>DynamicVisibility</CommandFlag>
7483
<CommandFlag>DefaultInvisible</CommandFlag>
@@ -84,7 +93,7 @@
8493
<Strings>
8594
<ButtonText>Duplicate Selection Reversed</ButtonText>
8695
<MenuText>Duplicate Selection &amp;Reversed</MenuText>
87-
<ToolTipText>Duplicates the selected lines in reverse.</ToolTipText>
96+
<ToolTipText>Duplicates the selected text in reverse.</ToolTipText>
8897
</Strings>
8998
</Button>
9099

@@ -166,31 +175,34 @@
166175
</Commands>
167176

168177
<CommandPlacements>
169-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidToggleComment" priority="0x7515">
178+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" priority="0x2795">
170179
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
171180
</CommandPlacement>
172-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" priority="0x7520">
181+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberUp" priority="0x3005">
173182
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
174183
</CommandPlacement>
175-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" priority="0x7525">
184+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberDown" priority="0x3010">
176185
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
177186
</CommandPlacement>
178-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" priority="0x2795">
187+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidToggleComment" priority="0x7515">
179188
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
180189
</CommandPlacement>
181-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x7516">
190+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" priority="0x7516">
182191
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
183192
</CommandPlacement>
184-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" priority="0x7517">
193+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x7517">
185194
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
186195
</CommandPlacement>
187-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" priority="0x7518">
196+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" priority="0x7518">
188197
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
189198
</CommandPlacement>
190-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberUp" priority="0x3005">
199+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" priority="0x7520">
191200
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
192201
</CommandPlacement>
193-
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberDown" priority="0x3010">
202+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" priority="0x7522">
203+
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
204+
</CommandPlacement>
205+
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" priority="0x7525">
194206
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
195207
</CommandPlacement>
196208
<!-- Go To ... Commands -->
@@ -210,6 +222,7 @@
210222
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" context="GUID_TextEditorFactory"/>
211223
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" context="GUID_TextEditorFactory"/>
212224
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" context="GUID_TextEditorFactory"/>
225+
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" context="GUID_TextEditorFactory"/>
213226
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" context="GUID_TextEditorFactory"/>
214227
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" context="GUID_TextEditorFactory"/>
215228
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" context="GUID_TextEditorFactory"/>
@@ -229,7 +242,7 @@
229242
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="W"/>
230243
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="F"/>
231244
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" editor="GUID_TextEditorFactory" mod1="Control" key1="D"/>
232-
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="D"/>
245+
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="D"/>
233246
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="J"/>
234247
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidGoToPreviousMember" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="VK_UP"/>
235248
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidGoToNextMember" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="VK_DOWN"/>
@@ -243,6 +256,7 @@
243256

244257
<!-- This is the guid used to group the menu commands together -->
245258
<GuidSymbol name="guidHotCommandsPackageCmdSet" value="{1023dc3d-550c-46b8-a3ec-c6b03431642c}">
259+
<IDSymbol name="cmdidDuplicateLine" value="0x1018" />
246260
<IDSymbol name="cmdidDuplicateSelection" value="0x1019" />
247261
<IDSymbol name="cmdidDuplicateSelectionReverse" value="0x1020" />
248262
<IDSymbol name="cmdidToggleComment" value="0x1021" />

HotCommands/Listeners/HotCommandsTextViewCreationListener.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ internal sealed class HotCommandsTextViewCreationListener : IVsTextViewCreationL
2828
[Import(typeof(IEditorOperationsFactoryService))]
2929
private IEditorOperationsFactoryService _editorOperationsFactory;
3030

31+
[Import]
32+
private ITextBufferUndoManagerProvider _undoProvider;
33+
3134
public void VsTextViewCreated(IVsTextView textViewAdapter)
3235
{
3336
IWpfTextView textView = EditorAdaptersFactoryService.GetWpfTextView(textViewAdapter);
3437

35-
HotCommandsCommandFilter commandFilter = new HotCommandsCommandFilter(textView, _aggregatorFactory, _globalServiceProvider, _editorOperationsFactory);
38+
HotCommandsCommandFilter commandFilter = new HotCommandsCommandFilter(textView, _aggregatorFactory, _globalServiceProvider, _editorOperationsFactory, _undoProvider);
3639
IOleCommandTarget next;
3740
textViewAdapter.AddCommandFilter(commandFilter, out next);
3841

HotCommands/Packaging/ReleaseNotes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Hot Commands for Visual Studio
2+
07-Aug-2022: v4.1.0 Added command: DuplicateLine (Issue #89 - Fabio Marmitt)
23
10-Jan-2022: v4.0.0 Upgraded to VS2022. Now only targeting VS2022+.
34
04-May-2020: v3.0.0 Now only targeting VS2017+. Handles Async loading (Issue #81/PR #82)
45
03-May-2020: v2.1.7 Updated build files so it builds in VS2015, VS2017 and VS2019

HotCommands/source.extension.vsixmanifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
33
<Metadata>
4-
<Identity Id="e8175b11-0e09-42b5-9c3c-ba7bfb53a311" Version="4.0.0" Language="en-US" Publisher="Justin Clareburt" />
4+
<Identity Id="e8175b11-0e09-42b5-9c3c-ba7bfb53a311" Version="4.1.0" Language="en-US" Publisher="Justin Clareburt" />
55
<DisplayName>Hot Commands</DisplayName>
66
<Description xml:space="preserve">A collection of commands and shortcuts for enhanced productivity in Visual Studio IDE. VS2022+</Description>
77
<MoreInfo>https://marketplace.visualstudio.com/items?itemName=JustinClareburtMSFT.HotCommandsforVisualStudio</MoreInfo>

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ Project for creating new commands and shortcuts for Visual Studio.
1616
<td>Ctrl+/</td>
1717
</tr>
1818
<tr>
19-
<td>Duplicate Code /<br /> Duplicate Reversed</td>
20-
<td>Duplicates the currently selected text, or the current line if no selection. <br /> Reversed: Same as Duplicate Code, but places the new code before the current selection (or line).</td>
21-
<td>Ctrl+D /<br /> Ctrl+Shift+D</td>
19+
<td>Duplicate Selection</td>
20+
<td>Duplicates the currently selected text, or the current line if no selection.</td>
21+
<td>Ctrl+D</td>
22+
</tr>
23+
<tr>
24+
<td>Duplicate Lines</td>
25+
<td>Duplicates the entire line(s) of the current selection, or the current line if no selection.</td>
26+
<td>Ctrl+Shift+D</td>
2227
</tr>
2328
<tr>
2429
<td>Edit.JoinLines</td>

0 commit comments

Comments
 (0)