Question:
Comrades, the problem is as follows: I'm used to sorting using
and (and not only) by the length of the string in my projects. That is, the code of the kind:
using System.Collections.Generic;
using System;
using System.Linq;
using System.IO;
I will definitely change to:
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
Only then will I calm down
So. I'm a little tired of doing this manually, so I want to ask: is there any lightweight extension (I suspect that ReSharper
can do this, but I don't consider it) for Visual Studio 2017
that can sort selected lines by their length?
Answer:
Actually, no one answered me, but I myself did not find a suitable extension. But who prevented me from writing my own? This is what I did. The extension is now hanging in the Marketplace , namely here .
Of course, I did not begin to formalize all this strongly, because I did this only for myself, but you never know such a trifle will ever come in handy for someone, and not everyone wants to install impressive extensions.
Well, let's at the same time consider an example of creating such a simple extension for the task at hand.
0) First, let's create a VSIX Project (Visual Studio extensions can be written in Visual C++, Visual Basic, C#. I wrote this case in C#). If the template for such a project is not in the list of available ones, install the appropriate item using the Visual Studio Installer.
1) Add a Custom Command
element to the project and name it " CommandSort ".
In the appeared class " CommandSort.cs " add the following code:
using System;
using System.Linq;
using Microsoft.VisualStudio.Shell;
using System.ComponentModel.Design;
namespace LineSorter
{
internal sealed class CommandSort
{
#region Var
private AsyncPackage Package { get; }
public int CommandId { get; } = 0x0100;
public static CommandSort Instance { get; private set; }
public static Guid CommandSet { get; } = new Guid("e9f69e2b-6313-4c2b-9765-1ddd6439d519");
#endregion
#region Init
private CommandSort(AsyncPackage Package, OleMenuCommandService CommandService)
{
this.Package = Package ?? throw new ArgumentNullException(nameof(Package));
CommandService = CommandService ?? throw new ArgumentNullException(nameof(CommandService));
CommandID menuCommandID = new CommandID(CommandSet, CommandId);
MenuCommand menuItem = new MenuCommand(Execute, menuCommandID);
CommandService.AddCommand(menuItem);
}
public static async System.Threading.Tasks.Task InitializeAsync(AsyncPackage Package)
{
ThreadHelper.ThrowIfNotOnUIThread();
Instance = new CommandSort(Package, await Package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService);
}
#endregion
#region Functions
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
TextSelection.GetSelection(Package).OrderBy(x => x.Length).ThenBy(x => x).ReplaceSelection();
}
#endregion
}
}
Our team's initializer, ID and GUID within the project will be set by VS automatically. In fact, we only need to change the Execute
method.
2) For more convenient work with the selected text, let's create the " TextSelection " class and add the following code to " TextSelection.cs ":
using EnvDTE;
using System;
using System.Linq;
using System.Windows;
using System.Collections.Generic;
using Microsoft.VisualStudio.TextManager.Interop;
namespace LineSorter
{
public static class TextSelection
{
#region Var
public static IServiceProvider ServiceProvider { get; set; }
#endregion
#region Functions
/// <summary>
/// Получим выделенный текст и избавим его от пробелов и табуляции в начале/конце строк
/// </summary>
public static IEnumerable<string> GetSelection(IServiceProvider ServiceProvider)
{
TextSelection.ServiceProvider = ServiceProvider;
IVsTextManager2 textManager = ServiceProvider.GetService(typeof(SVsTextManager)) as IVsTextManager2;
int result = textManager.GetActiveView2(1, null, (uint)_VIEWFRAMETYPE.vftCodeWindow, out IVsTextView view);
view.GetSelectedText(out string selectedText);
return selectedText.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim(new char[] { ' ', '\t' }));
}
public static void ReplaceSelection(this IEnumerable<string> Selections)
{
ReplaceSelection(Selections, ServiceProvider);
}
/// <summary>
/// Заменим выделенный текст на указанную коллекцию строк
/// </summary>
public static void ReplaceSelection(this IEnumerable<string> Selections, IServiceProvider ServiceProvider)
{
DTE dte = ServiceProvider?.GetService(typeof(DTE)) as DTE;
if (dte is null) return;
IDataObject obj = Clipboard.GetDataObject();
Clipboard.SetText(string.Join("\r\n", Selections));
dte.ExecuteCommand("Edit.Paste");
Clipboard.SetDataObject(obj);
}
#endregion
}
}
The logic of the code, I think, is quite clear. However, I will focus on the method of replacing the selected text with a new collection ( ReplaceSelection
). Initially the code looked like this:
DTE dte = ServiceProvider?.GetService(typeof(DTE)) as DTE;
if (dte is null) return;
EnvDTE.TextSelection selection = dte.ActiveDocument?.Selection as EnvDTE.TextSelection;
if (selection is null) return;
selection.Text = string.Join("\r\n", Selections);
And everything seems quite logical: just replace the selected text with a new one. In many manuals, I just saw this approach. However, I warn against this approach to working with selected text, as it works terribly slowly. When set, the EnvDTE.TextSelection.Text
property, for some reason, parses the entire inserted text, breaking it into semantic units. Let me explain: for example, we have 2 such using
'a, which we want to sort:
using Microsoft.VisualStudio.Shell.Interop;
using Task = System.Threading.Tasks.Task;
For our algorithm, it is only important that these are 2 lines. However, the specified property breaks the whole thing into a semantic set:
using, Microsoft, VisualStudio, Shell, Interop, using, Task, System, Threading, Tasks, Task
And we no longer have 2 lines, but 11 separately processed elements. As the number of strings being sorted increases, the number of processed semantic units grows catastrophically, so that a simple action with this approach begins to take an impressive amount of time.
That's why I used a little trick:
- I save the current state of the clipboard
- After that, I drive the combined collection of our strings into it
- And now I'm already running the paste function from VS itself (like Ctrl + V ). So the text is instantly inserted in place of the selection, after which it is formatted by the environment itself
- After this action, I return the previous state to the clipboard
Honestly, I don't know how good this solution is, but it is certainly faster than the standard way. If there are ideas for a more correct implementation – I will be glad to hear!
3) Well. Understood, actually, with the code. It remains to clean up the config ( CommandSortPackage.vsct
):
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h" />
<Extern href="vsshlids.h" />
<Commands package="guidCommandSortPackage">
<Groups>
<Group guid="guidCommandSortPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<!-- IDM_VS_CTXT_CODEWIN - контекстное меню VS -->
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
</Group>
</Groups>
<Buttons>
<Button guid="guidCommandSortPackageCmdSet" id="CommandSortId" priority="0x0100" type="Button">
<Parent guid="guidCommandSortPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPicArrows" />
<Strings>
<ButtonText>Сортировать линии</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\CommandSort.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
</Bitmaps>
</Commands>
<!-- Устанавливаем shortcut для нашей команды -->
<KeyBindings>
<KeyBinding guid="guidCommandSortPackageCmdSet" id="CommandSortId" editor="guidVSStd97" mod1="Control" mod2="Control" key1="E" key2="L" />
</KeyBindings>
<Symbols>
<GuidSymbol name="guidCommandSortPackage" value="{7fb18e2a-1a51-4dbb-b676-a3514e44823d}" />
<GuidSymbol name="guidCommandSortPackageCmdSet" value="{e9f69e2b-6313-4c2b-9765-1ddd6439d519}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<!-- Обратите внимание, что value равно значению, которое указано у нас в CommandSort.cs! -->
<IDSymbol name="CommandSortId" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{f2b4c3e0-a959-40c0-be9d-315e5ca1615c}">
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
</Symbols>
</CommandTable>
Thus, we add a button with the specified text and refer to our action in the VS context menu , and also associate hot keys with it: ( Ctrl + E ) + ( Ctrl + L )
4) That's all, our simple extension is ready. You can debug it using the experimental version of VS, or just build it, after which among a lot of packages you will find the coveted file with the *.vsix extension, with which you can either immediately install the extension in your studio, or even publish it)
And on this – that's all) Thank you for your attention)