Skip to content

Commit

Permalink
v4.1.0 - Implemented new command: DuplicateLines (#90)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
justcla authored Aug 7, 2022
1 parent c34d9ec commit 0305ad5
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 21 deletions.
67 changes: 64 additions & 3 deletions HotCommands/Commands/DuplicateSelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<SnapshotSpan> 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)
{
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions HotCommands/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion HotCommands/Hot Commands Shortcuts.vssettings
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
<Shortcut Command="Edit.GoToLastEditLocation" Scope="Global">Ctrl+Shift+Bkspce</Shortcut> <!-- In VS 15.8+ -->
<!--Duplicate Code-->
<Shortcut Command="Edit.Duplicate" Scope="Text Editor">Ctrl+D</Shortcut> <!-- v15.8+ -->
<Shortcut Command="Edit.DuplicateLines" Scope="Text Editor">Ctrl+Shift+D</Shortcut>
<Shortcut Command="Edit.DuplicateSelection" Scope="Text Editor">Ctrl+D</Shortcut>
<Shortcut Command="Edit.DuplicateSelectionReversed" Scope="Text Editor">Ctrl+Shift+D</Shortcut>
<!--<Shortcut Command="Edit.DuplicateSelectionReversed" Scope="Text Editor">Ctrl+Shift+D</Shortcut>-->
<!--Expand Selection-->
<Shortcut Command="Edit.IncreaseSelection" Scope="Text Editor">Ctrl+W</Shortcut> <!-- ExpandSelection in v15.7+ -->
<Shortcut Command="Edit.DecreaseSelection" Scope="Text Editor">Ctrl+Shift+W</Shortcut>
Expand Down
8 changes: 7 additions & 1 deletion HotCommands/HotCommandsCommandFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 25 additions & 11 deletions HotCommands/HotCommandsPackage.vsct
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@
</Strings>
</Button>

<Button guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" priority="0x0000" type="Button">
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<Strings>
<ButtonText>Duplicate Line</ButtonText>
<MenuText>Duplicate &amp;Line</MenuText>
<ToolTipText>Duplicates the selected text.</ToolTipText>
</Strings>
</Button>
<Button guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x0000" type="Button">
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
Expand All @@ -84,7 +93,7 @@
<Strings>
<ButtonText>Duplicate Selection Reversed</ButtonText>
<MenuText>Duplicate Selection &amp;Reversed</MenuText>
<ToolTipText>Duplicates the selected lines in reverse.</ToolTipText>
<ToolTipText>Duplicates the selected text in reverse.</ToolTipText>
</Strings>
</Button>

Expand Down Expand Up @@ -166,31 +175,34 @@
</Commands>

<CommandPlacements>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidToggleComment" priority="0x7515">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" priority="0x2795">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" priority="0x7520">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberUp" priority="0x3005">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" priority="0x7525">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberDown" priority="0x3010">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" priority="0x2795">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidToggleComment" priority="0x7515">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x7516">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" priority="0x7516">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" priority="0x7517">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" priority="0x7517">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" priority="0x7518">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" priority="0x7518">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberUp" priority="0x3005">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" priority="0x7520">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidMoveMemberDown" priority="0x3010">
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" priority="0x7522">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<CommandPlacement guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" priority="0x7525">
<Parent guid="guidStdEditor" id="IDG_VS_EDITOR_ADVANCED_CMDS"/>
</CommandPlacement>
<!-- Go To ... Commands -->
Expand All @@ -210,6 +222,7 @@
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidExpandSelection" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" context="GUID_TextEditorFactory"/>
<VisibilityItem guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" context="GUID_TextEditorFactory"/>
Expand All @@ -229,7 +242,7 @@
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidShrinkSelection" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="W"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidFormatCode" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="F"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelection" editor="GUID_TextEditorFactory" mod1="Control" key1="D"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateSelectionReverse" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="D"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidDuplicateLine" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="D"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidJoinLines" editor="GUID_TextEditorFactory" mod1="Control Shift" key1="J"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidGoToPreviousMember" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="VK_UP"/>
<KeyBinding guid="guidHotCommandsPackageCmdSet" id="cmdidGoToNextMember" editor="GUID_TextEditorFactory" mod1="Control Alt" key1="VK_DOWN"/>
Expand All @@ -243,6 +256,7 @@

<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guidHotCommandsPackageCmdSet" value="{1023dc3d-550c-46b8-a3ec-c6b03431642c}">
<IDSymbol name="cmdidDuplicateLine" value="0x1018" />
<IDSymbol name="cmdidDuplicateSelection" value="0x1019" />
<IDSymbol name="cmdidDuplicateSelectionReverse" value="0x1020" />
<IDSymbol name="cmdidToggleComment" value="0x1021" />
Expand Down
5 changes: 4 additions & 1 deletion HotCommands/Listeners/HotCommandsTextViewCreationListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions HotCommands/Packaging/ReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion HotCommands/source.extension.vsixmanifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<Metadata>
<Identity Id="e8175b11-0e09-42b5-9c3c-ba7bfb53a311" Version="4.0.0" Language="en-US" Publisher="Justin Clareburt" />
<Identity Id="e8175b11-0e09-42b5-9c3c-ba7bfb53a311" Version="4.1.0" Language="en-US" Publisher="Justin Clareburt" />
<DisplayName>Hot Commands</DisplayName>
<Description xml:space="preserve">A collection of commands and shortcuts for enhanced productivity in Visual Studio IDE. VS2022+</Description>
<MoreInfo>https://marketplace.visualstudio.com/items?itemName=JustinClareburtMSFT.HotCommandsforVisualStudio</MoreInfo>
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ Project for creating new commands and shortcuts for Visual Studio.
<td>Ctrl+/</td>
</tr>
<tr>
<td>Duplicate Code /<br /> Duplicate Reversed</td>
<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>
<td>Ctrl+D /<br /> Ctrl+Shift+D</td>
<td>Duplicate Selection</td>
<td>Duplicates the currently selected text, or the current line if no selection.</td>
<td>Ctrl+D</td>
</tr>
<tr>
<td>Duplicate Lines</td>
<td>Duplicates the entire line(s) of the current selection, or the current line if no selection.</td>
<td>Ctrl+Shift+D</td>
</tr>
<tr>
<td>Edit.JoinLines</td>
Expand Down

0 comments on commit 0305ad5

Please sign in to comment.