Страницы

Поиск по вопросам

воскресенье, 7 июля 2019 г.

Создание расширений VS, сортировка using по длине строки

Товарищи, проблема следующая: привык в своих проектах сортировать usingи (и не только) по длине строки. То есть код рода:
using System.Collections.Generic; using System; using System.Linq; using System.IO;
Я обязательно переделаю в:
using System; using System.IO; using System.Linq; using System.Collections.Generic;
И только тогда успокоюсь) Так вот. Я немного устал делать это вручную, так что хочу спросить: есть ли какое-либо легкое расширение (подозреваю, что ReSharper так умеет, но его я не рассматриваю) для Visual Studio 2017, способное выполнять сортировку выделенных строк по их длине?


Ответ

Собственно, никто мне так и не ответил, сам же я не нашел подходящего расширения. Но кто ж мне мешал написать свое? Сие я и сделал. Расширение теперь висит в Marketplace, а именно - здесь Оформлять сильно все это я, понятное дело, не стал, ибо делал сие только под себя, но мало ли кому-то когда-нибудь тоже пригодится такая мелочь, а ставить внушительные расширения хотят далеко не все. Ну и давайте заодно уж рассмотрим пример создания такого простейшего расширения на поставленной задаче.

0) Для начала создадим VSIX Project (расширения для Visual Studio можно писать на Visual C++, Visual Basic, C#. Я это дело писал на C#). Если шаблон такого проекта отсутствует в списке доступных, установите соответствующий пункт с помощью Visual Studio Installer.
1) Добавим в проект элемент Custom Command и назовем его "CommandSort". В появившийся класс "CommandSort.cs" добавим следующий код:
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 } }
Инициализатор нашей команды, ее ID и GUID в рамках проекта будут установлены VS автоматически. По сути от нас требуется изменить лишь метод Execute 2) Для более удобной работы с выделенным текстом создадим класс "TextSelection" и в "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 ///

/// Получим выделенный текст и избавим его от пробелов и табуляции в начале/конце строк /// public static IEnumerable 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[] { '
', '
' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim(new char[] { ' ', '\t' })); }
public static void ReplaceSelection(this IEnumerable Selections) { ReplaceSelection(Selections, ServiceProvider); } /// /// Заменим выделенный текст на указанную коллекцию строк /// public static void ReplaceSelection(this IEnumerable Selections, IServiceProvider ServiceProvider) { DTE dte = ServiceProvider?.GetService(typeof(DTE)) as DTE; if (dte is null) return; IDataObject obj = Clipboard.GetDataObject(); Clipboard.SetText(string.Join("
", Selections)); dte.ExecuteCommand("Edit.Paste"); Clipboard.SetDataObject(obj); } #endregion } }
Логика кода, думаю, вполне ясна. Однако заострю внимание на методе замены выделенного текста на новую коллекцию (ReplaceSelection). Изначально код выглядел так:
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("
", Selections);
И все кажется вполне логичным: просто заменяем выделенный текст на новый. Во многих мануалах я как раз такой подход и видел. Однако предостерегаю от такого подхода к работе с выделенным текстом, так как работает это жутко медленно. Свойство EnvDTE.TextSelection.Text при установке зачем-то парсит весь вставляемый текст, разбивая его на смысловые единицы. Поясню: к примеру, мы имеем 2 таких using'a, которые мы хотим отсортировать:
using Microsoft.VisualStudio.Shell.Interop; using Task = System.Threading.Tasks.Task;
Для нашего алгоритма важно лишь то, что это 2 строки. Однако указанное свойство разбивает все это дело на смысловой набор:
using, Microsoft, VisualStudio, Shell, Interop, using, Task, System, Threading, Tasks, Task
И мы имеем уже не 2 строки, а 11 отдельно обрабатываемых элементов. С ростом числа сортируемых строк число обрабатываемых смысловых единиц катастрофически растет, так что простенькое действие при таком подходе начинает занимать внушительный промежуток времени. Именно поэтому я воспользовался маленькой хитростью:
Я сохраняю текущее состояние буфера обмена После этого загоняю в него объединенную коллекцию наших строк А теперь уже запускаю функцию вставки из самой VS (как бы Ctrl + V). Так что текст моментально вставляется на место выделенного, после чего форматируется самой средой После сего действа возвращаю буферу обмена прошлое состояние
Честно - не знаю, насколько хорошо это решение, однако оно уж точно быстрее стандартного пути. Если будут идеи более правильной реализации - буду рад выслушать!
3) Что ж. Разобрались, собственно, с кодом. Осталось навести порядок в конфиге (CommandSortPackage.vsct):






Таким образом мы добавляем кнопку с указанным текстом и ссылающуюся на наше действие в контекстное меню VS, а также связываем с ним горячие клавиши: (Ctrl + E) + (Ctrl + L) 4) Вот и все, наше простенькое расширение готово. Вы можете отдебажить его с помощью экспериментальной версии VS, или же просто собрать его, после чего среди уймы пакетов Вы найдете заветный файлик с расширением *.vsix, при помощи которого Вы можете либо сразу установить расширение в свою студию, либо же даже опубликовать его)

А на сием - все) Спасибо за внимание)

Комментариев нет:

Отправить комментарий