Автоматическое выравнивание кода



    Доброго времени суток.

    Среди способов повышения читаемости кода, связанных с визуальным восприятием текста, можно выделить следующие:

    • Подсветка синтаксиса
    • Использование отступов
    • Вертикальное выравнивание

    Первые 2 способа хорошо себя зарекомендовали и применяются практически во всех современных IDE и продвинутых текстовых редакторах. Третий же метод не нашел такого широкого распространения. Этот пробел, как с теоретической, так и с практической точки зрения, я постараюсь восполнить в этой статье.



    Было бы ошибкой утверждать, что эта задача никогда не решалась. Беглый поиск показал, что существует множество реализаций, в том числе для используемого мной редактора Sublime Text.

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

    В связи с этим я решил искать собственный метод для решения задачи.

    Стоит заметить, что изначально я предполагал, что на решение потрачу не больше пары вечеров, но задача оказалась значительно сложнее, чем мне казалось на первый взгляд.

    Первым неудачным вариантом была попытка просто найти пары схожих токенов в двух соседних строках, а затем выровнять ее по ним. Причем необходимо было бы найти такие пары, которые, во-первых, не перекрывали бы друг друга, во-вторых, имели бы максимальную взвешенную сумму коэффициентов схожести. Однако следующий пример иллюстрирует нежизнеспособность данного подхода:

    // from 
    f(a, b + c)
    g(b + c, a)
    	
    // to
    f(a, b + c   )
    g(   b + c, a)
    


    В данном случае второй аргумент функции f и первый аргумент функции g столь похожи, что перевешивают похожесть запятых. Такое выравнивание неприемлемо, оно может сбить столку, так как для определения количества параметров функции придется пересчитывать количество запятых, расположенных в совершенно разным местах.

    Очевидно, что простым увеличением веса для пары (запятая, запятая) проблему не решить, так как сами параметры функции могут быть сколь угодно большими и, следовательно, иметь сколь угодно большой вес.

    Следовательно, необходимо учитывать не только степень похожести токенов, но и синтаксическую структуру выравниваемых строк.

    В этом случае возникает проблема, связанная с тем, что разные языки имеют свой собственный, порой уникальный, синтаксис, а инструмент хотелось бы видеть как можно более универсальным.

    Более того, в тексте программ могут встречаться места, выравнивать которые ни в коем случае нельзя. К ним можно отнести строковые константы и комментарии. Разные языки по разному распознают комментарии, так например С\С++ использует //, а Ruby и Python #. Поэтому единственным способом повышения универсальности является использование метода быстрого описания синтаксиса языка.

    Грамматика


    Основная идея использования грамматик заключается в построении дерева выражения таким образом, что каждый узел этого дерева выравнивался бы отдельно с соответствующим узлом другого дерева и только с ним.

    Структура, полученная из предыдущего примера:

    	f
    	(
    			a
    		,
    			b
    			+
    			c
    	)
    	
    	g
    	(
    			b
    			+
    			c
    		,
    			a
    	)
    




    Пунктирными линиями показаны узлы, между которыми будет происходить выравнивание.

    В связи с тем, что в задачи инструмента не входит интерпретация кода, то отпадает необходимость хранить полное описание синтаксиса каждого языка в программе. Наоборот, достаточно описать лишь те правила, которые мы хотим использовать для выравнивания. К ним могут быть отнесены аргументы функции, упомянутые выше, операции присваивания, скобочные конструкции.

    Среди требований, предъявляемых к грамматике, нужно выделить также следующее: любая последовательность токенов, так или иначе, должна быть принята грамматикой. Это требование позволит использовать единую грамматику для схожих языков.

    Грамматика, применяемая для построения показанной выше структуры, может быть описана в форме Бэкуса-Наура следующим образом:

    	main ::= expr
    	
    	expr ::= expr p 
    	         | p 
    	         | expr ',' expr
    	
    	p    ::= '(' expr ')' 
    	         |'(' ')' 
    	         | any_token_except(',', ')', '(') 
    


    Если же строка не может быть принята грамматикой (отсутствует закрывающая скобка, например), то на выходе распознавания будет линейная структура.

    Можно заметить, что выделение узлов в дереве соответствует не каждому правилу свертки грамматики, иначе, каждый токен находился бы в отдельном узле. Чтобы достичь желаемой структуры, необходимо выделить в грамматике те правила, применение которых должно образовывать новые узлы в дереве. В нашем случае такими правилами должны быть:

    	expr ::= expr ',' expr
    	p    ::= '(' expr ')'
    


    В ходе разработки была реализован LR-0 парсер, использующий описание грамматики для построения дерева выражения. Описание алгоритма построения парсера выходит за рамки данной статьи, более подробно с ним можно ознакомиться в книге: Ахо, Лам, Сети, Ульман — Компиляторы. Принципы, технологии, инструменты.

    Сравнение


    После получения дерева выражения необходимо производить выравнивание токенов с учетом их похожести. Похожая задача возникает в генетике при изучении генома. Выравнивание последовательностей — метод, основанный на размещении двух или более последовательностей мономеров ДНК, РНК или белков друг под другом таким образом, чтобы легко увидеть сходные участки в этих последовательностях[wiki].

    Один из способов для решения этой задачи в биоинформатике — применение динамического программирования, его я и взял за основу для задачи выравнивания токенов.

    Как и во всех задачах динамического программирования, исходная задача разбивается на более меткие, решение которых ищется рекурсивно. В данном случае, более мелкой задачей для выравнивания строк является выравнивание суффиксов этих строк. В результате рекурсивного прохода получается матрица, сопоставляющая индексам i, j токенов строк X и Y максимальный возможный суммарный вес схожести оставшихся суффиксов этих строк.
    Восстановление схожих пар по данной матрице не составляет труда.

    Описание для получения пар токенов из 2-х массивов на языке Ruby
    def match(x,y,i,j)
    		if(@cache[[i,j]] != nil) then
    			return @cache[[i,j]][0];
    		end
    
    		if x.size == i then
    			@cache[[i,j]] = [0, 3];
    			return 0;
    		end
    		if y.size == j then
    			@cache[[i,j]] = [0, 3];
    			return 0;
    		end
    
    		value = [];
    		value[0] = x[i].cmp(y[j]) + match(x,y,i+1,j+1)
    		value[1] = match(x, y, i  ,j+1)
    		value[2] = match(x, y, i+1,j  )
    
    		max_value = 0;
    		max_index = 0;
    		index = 0;
    		value.each{
    			|x|
    			if x > max_value then
    				max_value = x;
    				max_index = index; 
    			end
    			index += 1;
    		}
    		@cache[[i,j]] = [max_value, max_index];
    		return max_value;
    	end
    
    	def get_pairs(x,y, start = 0)
    		@cache = {};
    		match(x, y, start, start);
    		pairs = [];
    		i = j = start;
    		curr  = @cache[[i,j]][1]
    		while curr != 3
    			case curr
    			when 0 then
    				pairs += [[i,j]] if x[i].cmp(y[j]) > Float::EPSILON
    				i+=1;
    				j+=1;
    			when 1 then
    				j+=1;	
    			when 2 then
    				i+=1;	
    			end
    			curr = @cache[[i,j]][1]
    		end
    		return [@cache[[start, start]][0], pairs];
    	end
    



    Для того, чтобы обеспечить возможность выравнивания более чем двух строк, среди множество массивов пар выделяются цепочки, по которым и происходит выравнивание.



    На рисунке показана цепочка из запятых, по которым будет происходить выравнивание.

    Выделение токенов


    Выделение токенов из строки производится с помощью последовательности попыток применить регулярное выражение для соответствующего типа токенов. Эта последовательность должна быть устроена таким образом, чтобы первыми применялись более конкретные регулярные выражения, а затем более общие, выделяющее, возможно даже, отдельные символы. После успешного распознавания токену присваивается тип, соответствующий регулярному выражению. В проекте также реализована иерархия типов, которая призвана упростить построение грамматик и улучшить точность выравнивания.

    До сих пор не был затронут вопрос о том, как считать схожесть двух отдельно взятых токенов. К сожалению, мне не удалось найти строгого формального решения для этой задачи, поэтому схожесть токенов в текущей реализации определяется в основном эвристическими методами.

    В частности, схожесть идентификаторов определяется как отношение расстояния Левенштейна между идентификаторами к длине максимального из них.

    Значения по-умолчанию в этом случае следующие:

    	Правило                 Схожесть
    	
    	Совпадают значения      1
    	Совпадают типы          max(Levenshtein_dist / max_length, 0.1)
    	Ничего не совпадает     0
    


    Определение минимального отступа между токенами


    Хорошим стилем программирования считается простановка отступов в некоторых конструкциях языка.
    К таким случаям можно отнести простановку пробелов после запятой в аргументах функции, пробелы вокруг операторов сравнения и т.п…

    Однако варианты постановки пробелов отличаются не только для разных языков, но и разные программисты пользуются разными стилями.

    	f (a, b)
    	
    	//VS
    	
    	f(a, b)
    
    


    Чтобы упростить настройку автоматического проставления отступов в этом случае, было решено добавить возможность обучения.
    В качестве исходных данных используется множество правильно отформатированных строк. Для каждой последовательной пары токенов: запоминается их тип и количество пробелов. Затем полученные данные обобщаются согласно иерархии типов.

    Текущая реализация


    На момент написания статьи программа реализована как плагин к редактору Sublime Text 3. Среди особенностей следует отметить следующие:

    • Выравнивание выделенных строк с помощью «горячих» клавиш;
    • Защита от выравнивания строк с разными отступом. (Предполагается, что перед выравниванием строк отступы проставлены корректно.);
    • Поддержка разных вариантов грамматики, выделения токенов и иерархии типов в зависимости от используемого языка;
    • На момент написания статьи реализована пока только специализация для языка Cи 99-го стандарта и java.
    • Исходный код написан на Ruby, открыт и доступен на GitHub


    Среди планов на будущее хотелось бы отметить следующее:

    • Расширение списка специализированных грамматик;
    • Определение максимального числа пробелов при выравнивании на основе машинного обучения.
    • Возможность добавления строк в обучающее множество с помощью «горячих» клавиш.


    Примеры полученных результатов


    Примеры
    	switch (state)                                
    	{                                            
    	    case State.QLD: city = "Brisbane"; break; 
    	    case State.WA: city = "Perth"; break;     
    	    case State.NSW: city = "Sydney"; break;   
    	    default: city = "???"; break;             
    	}   
    	
    	// To
    	switch (state)                                
    	{
    	    case State.QLD:city = "Brisbane"; break;
    	    case State.WA :city = "Perth"   ; break;
    	    case State.NSW:city = "Sydney"  ; break;
    	    default       :city = "???"     ; break;
    	}    
    


    	// From
    	types[:space] = nil;
    	types[:quote] = nil;
    	types[:regexp] = nil;
    	types[:id] = nil;
    	types[:spchar] = nil;
    	
    	// To
    	
    	types[:space ] = nil;
    	types[:quote ] = nil;
    	types[:regexp] = nil;
    	types[:id    ] = nil;
    	types[:spchar] = nil;
    


    	// From
    	long int a = 2;
    	long long double b = 1;
    	const int doubl = 4; 
    	
    	// To
    	long  int         a     = 2;
    	long  long double b     = 1;
    	const int         doubl = 4;
    


    В последнем примере используется отделение описания типа на уровне грамматики, поэтому похожие токены double и doubl небыли выровнены между собой.



    Спасибо за внимание.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 50

      +4
      А есть ли возможность PIPE-интерфейса? Я например не пользуюсь саблаймом, но в Geany можно любой кусок кода отправить внешней программе на обработку и ваш плагин можно будет прикрутить в два счёта при условии наличия интерфейса командной строки.

      Тоже делал такую штуку, но выравнивает лишь переменные: github.com/scriptum/geany-scripts/blob/master/align-declarations.py
        +1
        Да, именно через PIPE и работает связка плагина на python и самого кода на Ruby.
        +23
        Жуть! За такие «выравнивания» руки надо программистам выдергивать. Во всех трех примерах первый код был значительнее приятней глазу и более читабельный, чем эти «крестики-нолики», в которые их превратили. Зачем из кода делать вышиванку? — Выглядит ужасно, восприятие кода теряется, появляется куча лишних табов/пробелов.
        Бывает, что иногда надо вставить себе чужой код. Первым делом приходится его восстанавливать после таких вот «улучшателей».
          +13
          Склонен не согласиться. Восстановление пробелов по-умолчанию значительно проще — это вопрос нажатия 1 кнопки в любой современной IDE.
            0
            Согласен только по последнему примеру. В первом выравнивал бы не все (default бы оставил), последний пример вообще бы не выравнивал. Но, как говорится, на вкус и цвет…
              +5
              В некоторых style-guide-ах можно наоборот встретить подобное требование, в частности, align-declarations. Из известных примеров — GNOME, где выравнивают даже аргументы функций. Кроме того, это помогает в редакторах с поддержкой длинного (на несколько строк) курсора. Скажем, прототип функции поменялся, и нужно в 10 строк добавить по одному одинаковому аргументу. Проблему вижу только при миграции (методом копипасты) кода из проекта с одним стилем кодирования в другой.
                +9
                Категорически не согласен. Скорость чтения выровненного кода значительно выше. Не говоря уже о скорости поиска опечаток.
                  +5
                  Вы просто привыкли читать код без выравнивания, вот вам и кажется, что он лучше. Объективно соотносить разные части кода и находить ошибки визуально проще, когда код выровнен.
                  +23
                  Когда только начинал программировать, мне маниакально хотелось выравнивать, выравнивать и выравнивать. Теперь я смотрю на выровненный код, и у меня кровоточат глаза, мне хочется всё вернуть обратно.

                  Я не хочу сказать, что выравнивание — это плохо. Это скорее дело вкуса и правил в вашем стандарте оформления кода, но я решил больше не выравнивать.

                  Кстати, в Python PEP8 тоже запрещает выравнивание.

                  Из вашего примера
                  // From
                  long int a = 2;
                  long long double b = 1;
                  const int doubl = 4; 
                      
                  // To
                  long  int         a     = 2;
                  long  long double b     = 1;
                  const int         doubl = 4;
                  

                  Я бы оставил без выравнивания. :)

                    +2
                    Это дело конкретных данных, которые выравниваются. Если они ближе к табличной природе — выравнивание улучшает восприятие. Но выравнивание всего подряд (например, аргументов разных функций, как в первом неудачном примере из статьи), я считаю, однозначно плохо.

                    Поэтому, пожалуй, желательно было бы иметь возможность динамически проставлять признак выравнивания для таких наборов данных, нежели отдавать это на откуп IDE, которая принимает решение на основе синтаксического анализа.
                      +2
                      Текущий вариант реализации и работает в ручном режиме. Т.е. необходимо сначала выделить строки, которые требует выравнять, а потом нажать хоткей.
                      –1
                      Хм. я бы так написал:

                      long int a         = 2;
                      long long double b = 1;
                      const int doubl    = 4;
                      

                        0
                        Я думаю, кровоточат от того, что вы подсознательно пытаетесь представить, как трудно будет поддерживать правильное выравнивание в вашей любимой IDE, где это не делается автоматически. Если подобное делается само собой или одной хоткеей, то проблем никаких.
                          +2
                          А как насчет массового переименования типов?

                          до переименования:
                          Type1          a = 1;
                          long int       b = 2;
                          BlaBlaType2    c = 3;
                          const int      d = 4;
                          

                          после переименования:
                          Type1Renamed          a = 1;
                          long int       b = 2;
                          BananaBananaType2  c = 3;
                          const int      d = 4;
                          
                            0
                            Я полагаю, пример с кучей разных типов скорее был как демонстрация возможностей системы. Никто не заставляет так делать, если не нравится. А в целом подружить с рефакторингами, я думаю, можно. Можно сделать кнопки типа «автоматически выравнивать/не выравнивать определения переменных, если потребовалось вставлять не более X пробелов подряд». Главное, что автор движок написал, а прикрутить тонкую настройку — дело техники.
                              +1
                              Как тут в комментариях заметили, выравнивание хорошо подходит для table-like кода. Косить весь код под одну гребёнку выравнивания часто просто не нужно, потому что читабельнее он от этого зачастую не становится.

                              Инструмент будет полезен для выравнивания отдельных участков, например, выделенных пользователем, и которые меняются не часто (те же константы с табличными данными и т. п.).
                        +28
                        Выравнивание затрудняет код ревью и аннотирование изменений в файле. Представьте, что вы добавили новое свойство в объект и его название длиннее, чем все предыдущие. Теперь вы выровняли код — все строчки в объекте будут помечены как измененные. Что означает — ревьюеру кода нужно дополнительно напрягать зрение и вычитывать, что в остальных строчках действительно изменились только вайтспейсы. Если вы захотите проаннотировать файл на изменения — вы будете автором последних изменений всех строк этого объекта.
                          +12
                          Бесполезные \s символы diff не должен учитывать, вообще diff по хорошему должен семантику языка понимать.
                            +1
                            Согласен, а то привыкли к тупым универсальным инструментам. И мысли даже нет стремиться к лучшему.
                              +4
                              diff -w
                                0
                                Не совсем:
                                some_function(10, 30); // тут можно забить на пробелы
                                some_other_function('booooo ');// тут нельзя

                                так что надо синтаксический diff делать
                                  0
                                  Выравнивание не будет затрагивать строковые константы, а только те места, которые не влияют на выполнение.
                                    +3
                                    Я про то что «diff -w» не покатит
                                +1
                                Он не может не учитывать, когда накладывается патч. А при простом сравнении глазами можно и выкинуть (см. выше). Проблема, конечно, есть, но никто ещё не умер.
                                  0
                                  Кстати, что вы можете посоветовать из diff-ов, понимающих скажем c++?
                                    0
                                    Именно я особо ничего не могу, но гугль (вероятно я уже это гуглил и у меня персонализированная выдача ) подсказывает SmartDifferencer и cmpp.coodesoft.com. А ещё я ребят из jetbrains всё на это подбиваю youtrack.jetbrains.com/issue/IDEABKL-725, благо у них уже есть AST, а с ним дальше всё в принципе понятно +-
                                      0
                                      Есть платная программа Beyond Compare. У неё есть опция «Ignore Unimportant Differences». Уже много лет пользуюсь ей для просмотра разницы и мерджа. Мне кажется, она своих денег стоит.
                                      Пример работы на python-коде


                                      На С++ коде работает тоже хорошо.
                                        0
                                        А точно оно работает для разницы
                                        — test(" some string ")
                                        + test(«some string»)
                                        ?
                                          +1
                                          Вы имеете в виду файлы патчей? Она может создавать отчёты в разных форматах, в том числе в формате патчей.

                                          Вот так это выглядит




                                          Как видно, строки с изменениями пробелов в патче не учитываются.
                                            0
                                            Я имею ввиду пробел внутри строки-аргумента функции. Он значим.
                                            То есть как выглядит сравнение файла 1:
                                            somefun(" test ")

                                            с файлом, где пробелы _в строке_ удалены:
                                            somefun("test")

                                              +1
                                              Я вас понял. Изменения в строках учитываются как важные изменения и входят в патч, то есть они не определяются как «Unimportant».
                                              Скрытый текст

                                                0
                                                Для каких языков есть анализ?
                                                  +3
                                                  Из коробки C/C++, Java, Python, Perl, COBOL, Delphi, HTML, XML, SQL плюс всякие экзотические. Можно добавлять новые форматы, создавая собственные грамматики.
                                                    +1
                                                    Спасибо вам за отличную информацию.
                                  +8
                                  Автор, вы подняли холиварную тему. Щас люди, которые выравниванием не пользуются, дабы победить это явление, накидают вам минусов, и хорошо еще если не в карму.

                                  Я лично вас поддерживаю. Хотелось бы иметь такой инструмент для js и coffee в webstorm-е.
                                    +1
                                    Насчет переменных не скажу, но вот объекты и массивы PhpStorm умеет, значит и WebStorm должен уметь. Потыкайте в Settings -> Code Style -> JavaScript, а дальше (если не меняли, в Линукс) Ctrl + Alt + L и будет выравнивать.
                                    0
                                    +1 вам на github-е
                                      +1
                                      В таком деле как выравнивание хорошо знать меру. Лично я выравниваю, но если строки не слишком «разбежались», в противном случае от выравнивания получается не польза, а вред, т.к. читаемость, наоборот, падает.
                                      Например, я выравниваю, если у меня есть два схожих выражения, в которых длины имён переменных отличаются на 1..2 символа. Тогда можно добить одну из строк, чтобы привести её в соответствие с другой. Если же есть блок объявления переменных, в котором одна переменная имеет, скажем, четыре символа в имени, а другая 15, то я не буду выравнивать значения по умолчанию, просто потому что это один блок. Плюс, если придёт ещё одна переменная, ещё более длинная, то две предыдущих декларации придётся приводить в соответствие.

                                      Короче, иногда, такое выравнивание может привнести в читаемость плюсы, и я считаю, что было бы очень круто эту технику знать. Но с другой стороны, её возможности далеко не безграничны.
                                        0
                                        Автор молодец, за статью и код на github плюс. Но сама идея как уже отметили выше холиварная. По своему опыту скажу, что в коде стараюсь избегать использования выравнивания, потому что считаю его злом в большинстве случаев. Такие украшательства «полезны» только если вы консервируете ваш код и больше никогда его не изменяете. Но, очевидно, чаще всего это не так и позже его приходится править (и не только автору, но и другим людям). Так вот после малейшего рефакторинга все такие «произведения искусства» приходится реставрировать.

                                        P.S. Стив Макконнелл в данном случае дает одно простое правило: «Исправление одной строки не должно приводить к изменению нескольких других».
                                          +7
                                          Нет, я все понимаю, на вкус и цвет фломастеры разные…

                                          Но почему нет пробела после : в switch?
                                            +5
                                            Весьма спорные подходы к выравниванию. Надо заметить, что я сам выравниваю таблицы значений, если их приходится хардкодить, чтобы было удобней воспринимать данные в их табличном представлении и столбцы не скакали влево-вправо. Но отрывать точку с запятой от того к чему она относится — нехорошо. Это как отрывать запятую от слова, после которого она идет в предложении. Выравнивание квадратных скобок, тоже весьма спорно, ровно как и дополнительные пробелы в именах типов. Хотя вы правы, что это дело вкуса и при достаточном тюнинге можно заставить работать алгоритм по своему вкусу.
                                              0
                                              А как вы решаете вопрос совместной работы товарищей с разным подходом в стиле?
                                              Каждый чекаут требует переформатирования перед сессией работы?
                                              Или как?
                                                +2
                                                Не вижу разницы между разными подходами к выравниванию и общим стилем кода в этом случае. Проблема должна решаться соглашением.
                                                  +2
                                                  Для таких товарищей пишутся Coding Style Guide-ы, непокорных просто не пускают на сервер до тех пор пока не исправят код. В Гугле ещё веселее — перед пушем ваш код сканирует валидатор, если он ему не нравится — придётся переделывать.
                                                    0
                                                    Это как? Если file.lines != uncrustify(file.lines), то отбой?
                                                    Что ли это негуманно…
                                                +1
                                                забыл статью но создатель nodejs однажды написал, что если вы е6ете мозг выравнием то это все не программирование а чушь несусветная
                                                я его понял
                                                и с тех пор я равношки выравнивать перестал/ и счастлив!
                                                  0
                                                  IMHO правильнее всего что бы не холиварить хорошо выравнивание или плохо, сначала надо выбрать чего мы хотим достичь, ну там уменьшения перечитывания человеком строк кода, скоростью чтения и скоростью проглядывания кода, ещё что-нибудь про удобство редактирования и наверно про понимание кода, затем напридумывать метрики всего этого, измерять для каждого человека и делать как ему удобно(ну там при загрузке/выгрузке из/в VCS переформатировать для каждого). Благо сейчас пошли стартапы предлагающие ±точные(погрешность еденицы mm) и не шибко дорогие(100$) eye-tracking девайсы, например theeyetribe.com

                                                    +2
                                                    my $a = 1;
                                                    my $abc = "123";
                                                    my $abcdef = 3;
                                                    


                                                    после perltidy:

                                                    my $a      = 1;
                                                    my $abc    = "123";
                                                    my $abcdef = 3;
                                                    


                                                    Задумываться о выравнивании во время написания кода это издевательство — думать надо о коде, а не о пробелах. Но вот при чтении правильное выравнивание, в том числе выравнивание деклараций переменных очень удобно.

                                                    Потому код написал, и сразу perltidy на него.
                                                      0
                                                      Есть давольно удобный плагин для vim — github.com/junegunn/vim-easy-align

                                                      Only users with full accounts can post comments. Log in, please.