воскресенье, 26 июня 2011 г.

vim: практические шаги для комфортного использования

Ниже приводятся четыре практических совета, которые являются в большей степени методами устранения недостатков, присущих vim и его известным плагинам, нежели самостоятельными решениями. Поскольку в трех советах речь пойдет о модификации плагинов, будут представлены патчи, соответствующие текущим версиям этих плагинов. Патчи можно накладывать автоматически или вручную. В первом случае достаточно сохранить патч в файле, скажем c.vim.patch, затем перейти в директорию, в которой находится файл c.vim (в данном случае это $HOME/.vim/plugin/) и выполнить команду:
patch -p0 < /path/to/c.vim.patch
1. Поддержка директорий проектов в c.vim. Под поддержкой проекта будем понимать возможность использования различных шаблонов c.vim в зависимости от того, какому проекту принадлежит редактируемый файл. Как правило, файлы проекта расположены в отдельной директории, а сами проекты могут предъявлять разные требования к оформлению элементов, таких как заголовки файлов. В c.vim имеется поддержка отдельных проектов с помощью макроса STYLE (см. документацию c.vim), однако ее реализация имеет недостатки. В частности, этот подход подразумевает централизованное хранение всех шаблонов в директории ~/.vim/c-support/templates/. Нормальная поддержка проектов должна предполагать возможность хранения независимого набора шаблонов в директории проекта - это более гибкий и естественный подход.

Следующий патч (относительно c.vim версии 5.13) реализует возможность хранения шаблонов c.vim в директории проекта:
--- c.vim 2011-01-26 00:12:31.000000000 +0300
+++ c.vim.new 2011-06-26 13:07:16.000000000 +0400
@@ -327,6 +327,10 @@
 
 let s:C_SourceCodeExtensionsList = split( s:C_SourceCodeExtensions, '\s\+' )
 
+if exists( 'g:C_ProjectDirs' )
+ let s:C_ProjectList   = split( g:C_ProjectDirs, ',' )
+endif
+
 "------------------------------------------------------------------------------
 
 "------------------------------------------------------------------------------
@@ -2702,6 +2706,16 @@
     let s:C_FileVisited   = []
   let messsage     = ''
   "
+
+  for proj in s:C_ProjectList
+   if stridx(getcwd(), proj) != -1
+    if filereadable( proj."/.cvim.templates/Templates" )
+     call C_ReadTemplates( proj."/.cvim.templates/Templates" )
+     return
+    endif
+   endif
+  endfor
+
   if s:installation == 'system'
    "
    if filereadable( s:C_GlobalTemplateFile )
Здесь вводится новая глобальная переменная C_ProjectDirs, которая представляет собой список проектов, разделенных запятыми, для которых могут быть предложены отдельные наборы шаблонов. Например, в C_ProjectDirs добавим проект /path/to/proj1, далее в указанной директории создадим директорию .cvim.templates/, скопируем туда шаблоны из ~/.vim/c-support/templates/ и отредактируем по вкусу. Теперь при редактировании файлов в директории /path/to/proj1 и любой ее поддиректории будут использоваться новые шаблоны. При редактировании файла, не входящего ни в один из указанных в C_ProjectDirs проектов, будут использоваться шаблоны по умолчанию.

Переменную C_ProjectDirs можно определить в ~/.vimrc, например так:
let g:C_ProjectDirs = "/path/to/proj1,".$PROJ2WORKDIR
Здесь указаны 2 проекта: /path/to/proj1 и проект, определенный через переменную среды PROJ2WORKDIR, проекты разделены запятой.

2. Проблема взаимодействия fuzzyfinder и taglist. Taglist - самый популярный плагин vim. Он предоставляет отдельное окно для навигации по тегам исходного кода в открытых файлах. Это очень старый плагин и, к сожалению, давно не обновлялся, поэтому ему присущи недостатки, в частности, его сложно использовать при наличии нескольких табов (которые стали доступны в vim 7.0). Fuzzyfinder - очень интересный плагин, который легко использовать для навигации по файловой системе, тегам, закладкам и др. Особенностью данного плагина является то, что он не использует отдельное окно, а открывает временное окно поиска в левом верхнем углу терминала по запросу. К сожалению, при определенных настройках taglist, эти два плагина не дружат, что выражается в выводе нового буфера в окно taglist после закрытия окна поиска fuzzyfinder (см. здесь). Починить это поведение можно с помощью простого изменения в файле ~/.vim/autoload/l9/tempbuffer.vim (библиотека l9 поставляется вместе с fuzzyfinder, патч приведен относительно версии l9 1.1):
--- tempbuffer.vim 2010-09-28 23:55:28.000000000 +0400
+++ tempbuffer.vim.new 2011-06-26 13:13:54.000000000 +0400
@@ -22,7 +22,9 @@
   endif
   if bufnr('%') == s:dataMap[a:bufname].bufNr && winnr('#') != 0
     " if winnr('#') returns 0, "wincmd p" causes ringing the bell.
-    wincmd p
+    " my cmt: 'wincmd p' works badly with taglist - using 'wincmd j' instead
+    "wincmd p
+    wincmd j
   endif
 endfunction
 
@@ -43,7 +45,8 @@
 " a:listener:
 "   a:listener.onClose(written)
 function l9#tempbuffer#openScratch(bufname, filetype, lines, topleft, vertical, height, listener)
-  let openCmdPrefix = (a:topleft ? 'topleft ' : '')
+  " my cmt: open fuf buffer above current window ('leftabove' instead 'topleft')
+  let openCmdPrefix = (a:topleft ? 'leftabove ' : '')
         \           . (a:vertical ? 'vertical ' : '')
         \           . (a:height > 0 ? a:height : '')
   if !exists('s:dataMap[a:bufname]') || !bufexists(s:dataMap[a:bufname].bufNr)
Данный патч делает еще одно, на мой взгляд, полезное изменение в поведении fuzzyfinder: теперь окно поиска будет открываться не в левом верхнем угла терминала, а вверху окна, в котором осуществляется редактирование.

3. Поддержка ack в grep.vim. ack - это grep для пограммистов. Это очень удобная утилита для поиска текста в директории исходников, она знает о служебных директориях CVS, Subversion и др. и игнорирует их, поддерживает регулярные выражения perl и обладает другими полезными свойствами. В vim существует плагин, который поддерживает ack, однако он реализует простейшую обертку встроенного в vim grep и "мусорит" в stdout. Плагин grep.vim реализован лучше, однако не обладает поддержкой ack. Следующий патч реализует это поддержку в grep.vim (относительно версии grep.vim 1.9):
--- grep.vim 2011-06-26 13:19:02.000000000 +0400
+++ grep.vim.new 2011-06-26 13:22:22.000000000 +0400
@@ -317,6 +317,10 @@
     let Agrep_Path = 'agrep'
 endif
 
+if !exists("Ack_Path")
+    let Ack_Path = 'ack'
+endif
+
 " Location of the find utility
 if !exists("Grep_Find_Path")
     let Grep_Find_Path = 'find'
@@ -731,7 +735,7 @@
         let grep_opt = g:Grep_Default_Options
     endif
 
-    if a:grep_cmd != 'agrep'
+    if a:grep_cmd != 'agrep' && a:grep_cmd != 'ack'
         " Don't display messages about non-existent files
         " Agrep doesn't support the -s option
         let grep_opt = grep_opt . " -s"
@@ -749,6 +753,9 @@
     elseif a:grep_cmd == 'agrep'
         let grep_path = g:Agrep_Path
         let grep_expr_option = ''
+    elseif a:grep_cmd == 'ack'
+        let grep_path = g:Ack_Path
+        let grep_expr_option = ' -H --nogroup --nocolor'
     else
         return
     endif
@@ -763,7 +770,7 @@
                         \ g:Grep_Shell_Quote_Char
     endif
 
-    if filenames == ""
+    if filenames == "" && a:grep_cmd != 'ack'
         if v:version >= 700
             let filenames = input("Search in files: ", g:Grep_Default_Filelist,
                         \ "file")
@@ -777,8 +784,12 @@
 
     " Add /dev/null to the list of filenames, so that grep print the
     " filename and linenumber when grepping in a single file
-    let filenames = filenames . " " . g:Grep_Null_Device
-    let cmd = grep_path . " " . grep_opt . " -n "
+    if a:grep_cmd != 'ack'
+        let filenames = filenames . " " . g:Grep_Null_Device
+        let cmd = grep_path . " " . grep_opt . " -n "
+    else
+        let cmd = grep_path . " " . grep_opt
+    endif
     let cmd = cmd . grep_expr_option . " " . pattern
     let cmd = cmd . " " . filenames
 
@@ -810,6 +821,9 @@
 command! -nargs=* -complete=file Ragrep
             \ call s:RunGrepRecursive('Ragrep', 'agrep', 'set', <f-args>)
 
+command! -nargs=* -complete=file Ack
+            \ call s:RunGrep('Ack', 'ack', 'set', <f-args>)
+
 if v:version >= 700
 command! -nargs=* -complete=file GrepAdd
             \ call s:RunGrep('GrepAdd', 'grep', 'add', <f-args>)
Теперь, для того чтобы выполнить рекурсивный поиск вхождения sample в текущей директории из vim, достаточно ввести команду
:Ack sample
Тот же поиск, но во всех файлах с расширением .cc в текущей директории:
:Ack sample *.cc
4. Навигация между окнами с помощью комбинаций клавиш Ctrl-Arrow. Это очень полезный совет, который можно найти на многих ресурсах в сети. Суть идеи в том, чтобы при наличии нескольких открытых окон переключаться между ними простой комбинацией клавиш Ctrl-Up и Ctrl-Down. Такое поведение требует некоторой настройки, в частности, значение bufhidden должно быть равным delete, чтобы при навигации скрытые буферы (т.е. те файлы, которые ранее редактировались в открытых окнах, но были в дальнейшем скрыты новыми файлами) не открывались в отдельных окнах. Итак, настройки для данного поведения, которые следует поместить в ~/.vimrc выглядят так:
set switchbuf=usetab
autocmd BufEnter * setlocal bufhidden=delete
nmap <C-up>     :sbn<CR>
nmap <C-down>   :sbp<CR>
nmap <C-left>   :tabp<CR>
nmap <C-right>  :tabn<CR>
Последние два маппинга добавляют возможность навигации между табами по комбинациям клавиш Ctrl-Left и Ctrl-Right и не имеют отношения к рассматриваемой далее проблеме.

А проблема заключается в том, что если некоторый файл попал по той или иной причине в список bufhidden, то при его повторном открытии попасть в его окно с помощью комбинаций Ctrl-Up или Ctrl-Down становится невозможно. Это означает, что, скорее всего, vim не удаляет файл из списка bufhidden при его повторном открытии. Лечится это добавлением еще одной авто-команды в ~/.vimrc:
autocmd BufEnter * if empty(&buftype) | setlocal buflisted | endif
Update: Кажется редактор blogger превратил символы табуляции в патчах в пробелы :( Так что для автоматического наложения патчей теперь придется использовать опцию -l, например так:
patch -l -p0 < /path/to/c.vim.patch