Сегодня поговорим о создании дополнений для VIM.
Недавно у меня возникла идея вкрутить в него поддержку cmake проектов для удобной навигации по файлам. С этой задачей, конечно, вполне справится NERD Tree, но в последнем нельзя оперировать исключительно файлами проекта.
Ахтунг: Автор статьи впервые познакомился с Vim Script. Он не гарантирует, что вы не упадете в обморок после прочтения статьи. Любые пожелания касательно кода оставляйте в комментариях.
Плагин управления cmake проектами должен по команде cmake создать необходимые файлы для сборки в папке «build» и отобразить панельку с деревом файлов, нажимая на элементы которых можно легко достучаться к файлам исходников.
И так, начнем реализовывать.
Если залезть глубоко в недрах файлов, созданных с помощью cmake, можно обнаружить в каком месте он хранит список зависимых исходных файлов. Сделаем поиск на наличие строк с cppшниками полутопорным образом:
Я обнаружил в DependInfo.cmake переменную с таким содержанием
Находим все файлы DependInfo.cmake в дирректории и находим полные пути к файлам с помощью Perl скрипта.
Полный исходник тут.
Прежде чем привязать эти функции к плагину, разберемся в иерархии директорий в ~/.vim.
Так как наш плагин должен загружаться при каждом запуске редактора, поместим его в ~/.vim/plugin. Назвем файл как cmake-project.vim.
Проверим наличие perl интерпретатора:
Создадим функцию для генерации дерева файлов. Функции, а также переменные можно создавать с разными областями видимости (прочитать об этом можно тут). Эта функция в начале создает новое окно и буфер с именем «CMakeProject».
Для определения является ли наш текущий буфер панелью с деревом файлов, объявим переменную (с областью видимости внутри плагина) с именем буфера.
А теперь привяжем Perl скрипт к плагину. Скрипт поместим в директорию ~/.vim/plugin/cmake-project чтобы use lib ее смог найти. Получаем список файлов из функции cmakeproject::cmake_project_files и поместим в вимовский список.
Далее на базе этих данных строим дерево. В vim есть несколько структур данных: хэши и списки. Поэтому директорию представим как ключ в хэше. Если ключ хэша указывает на что-нибудь (не 1), то это директория, а если на 1, то это файл, находящийся в директории.
Код, приведенный ниже преобразовывает строку вида "/home/paranoik/main.cpp" в структуру вида {'home': {'paranoik': {'main.cpp': 1}}, где {key: value} — хэш с 1 парой ключ-значение.
Теперь определим функцию для отображения дерева в буфере. В зависимости от уровня иерархии определяются отступы в виде пробелов (за это отвечает фунция s:cmake_project_indent).
Привяжем функцию s:cmake_project_window() к команде CMakePro
Также нам необходима команда для генерации cmake файлов.
При движении курсора по панели, должен открываться файл под курсором. Создадим функцию s:cmake_project_cursor_moved() и привяжем ее к сигналу CursorMoved.
Для того, чтобы функция работала только с буфером панели, проверяем его имя перед выполнением.
Получаем данные текущей строки и выделяем слово под курсором.
Определим директорию, в которой находится файл через отступы. Если файл является элементом n-того уровня, то директория, в которой находится файл является ближайщий элемент сверху с отступами n-1-ого уровня.
Открываем необходимый файл
В итоге получилось:

Исходники брать здесь:
Недавно у меня возникла идея вкрутить в него поддержку cmake проектов для удобной навигации по файлам. С этой задачей, конечно, вполне справится NERD Tree, но в последнем нельзя оперировать исключительно файлами проекта.
Ахтунг: Автор статьи впервые познакомился с Vim Script. Он не гарантирует, что вы не упадете в обморок после прочтения статьи. Любые пожелания касательно кода оставляйте в комментариях.
Плагин управления cmake проектами должен по команде cmake создать необходимые файлы для сборки в папке «build» и отобразить панельку с деревом файлов, нажимая на элементы которых можно легко достучаться к файлам исходников.
И так, начнем реализовывать.
Если залезть глубоко в недрах файлов, созданных с помощью cmake, можно обнаружить в каком месте он хранит список зависимых исходных файлов. Сделаем поиск на наличие строк с cppшниками полутопорным образом:
grep ".*\.cpp" -R build/
Я обнаружил в DependInfo.cmake переменную с таким содержанием
SET(CMAKE_DEPENDS_CHECK_CXX "/home/..../brushcombo.cpp" "/home/.../build/CMakeFiles/kdots.dir/brushcombo.o" ... )
Находим все файлы DependInfo.cmake в дирректории и находим полные пути к файлам с помощью Perl скрипта.
sub cmake_project_files { my $dir = shift; my @dependencies = File::Find::Rule->file() ->name("DependInfo.cmake") ->in($dir); my @accum = (); foreach my $filename(@dependencies) { open(FILE, $filename); my @data = <FILE>; push (@accum, src_files(\@data)); close(FILE); } return @accum; } sub src_files { my @result = (); foreach my $line(@{(shift)}) { if ($line =~ m/\s*\"(([a-zA-Z_\/]+)\/([a-zA-Z_]+\.(cpp|cc))).*/) { push(@result, $1); } } return @result; }
Полный исходник тут.
Прежде чем привязать эти функции к плагину, разберемся в иерархии директорий в ~/.vim.
- plugin — сюда помещаются плагины, которые должны загружаться при каждом запуске VIM
- ftplugin — сюда помещаются плагины, которые запускаются только для определенных типов файлов
- autoload — для хранения общих функций
- syntax — подсветка синтаксиса
Так как наш плагин должен загружаться при каждом запуске редактора, поместим его в ~/.vim/plugin. Назвем файл как cmake-project.vim.
Проверим наличие perl интерпретатора:
if !has('perl') echo "Error: perl not found" finish endif
Создадим функцию для генерации дерева файлов. Функции, а также переменные можно создавать с разными областями видимости (прочитать об этом можно тут). Эта функция в начале создает новое окно и буфер с именем «CMakeProject».
function! s:cmake_project_window() vnew badd CMakeProject buffer CMakeProject "Нужно указать, что это не файл, чтобы при выходе, VIM не заставлял сохранять изменения setlocal buftype=nofile ...
Для определения является ли наш текущий буфер панелью с деревом файлов, объявим переменную (с областью видимости внутри плагина) с именем буфера.
let s:cmake_project_bufname = bufname("%")
А теперь привяжем Perl скрипт к плагину. Скрипт поместим в директорию ~/.vim/plugin/cmake-project чтобы use lib ее смог найти. Получаем список файлов из функции cmakeproject::cmake_project_files и поместим в вимовский список.
perl << EOF "Тоже самое можете сделать для Python или для Ruby use lib "$ENV{'HOME'}/.vim/plugin/cmake-project"; use cmakeproject; my $dir = VIM::Eval('g:cmake_project_build_dir'); my @result = cmakeproject::cmake_project_files($dir); VIM::DoCommand("let s:cmake_project_files = []"); foreach $filename(@result) { if (-e $filename) { VIM::DoCommand("call insert(s:cmake_project_files, \'$filename\')"); } } EOF
Далее на базе этих данных строим дерево. В vim есть несколько структур данных: хэши и списки. Поэтому директорию представим как ключ в хэше. Если ключ хэша указывает на что-нибудь (не 1), то это директория, а если на 1, то это файл, находящийся в директории.
Код, приведенный ниже преобразовывает строку вида "/home/paranoik/main.cpp" в структуру вида {'home': {'paranoik': {'main.cpp': 1}}, где {key: value} — хэш с 1 парой ключ-значение.
let s:cmake_project_file_tree = {} for fullpath in s:cmake_project_files let current_tree = s:cmake_project_file_tree let cmake_project_args = split(fullpath, '\/') let filename = remove(cmake_project_args, -1) for path in cmake_project_args if !has_key(current_tree, path) let current_tree[path] = {} "Создаем пустой хэщ endif let current_tree = current_tree[path] endfor let current_tree[filename] = 1 endfor call s:cmake_project_print_bar(s:cmake_project_file_tree, 0)
Теперь определим функцию для отображения дерева в буфере. В зависимости от уровня иерархии определяются отступы в виде пробелов (за это отвечает фунция s:cmake_project_indent).
function! s:cmake_project_print_bar(tree, level) for pair in items(a:tree) if type(pair[1]) == type({}) "Если это директория let name = s:cmake_project_indent(a:level) . "-" . pair[0] call append(line('$'), name . "/") "Выводим в виде "-<dir>/" let newlevel = a:level + 1 call s:cmake_project_print_bar(pair[1], newlevel) "Отображаем поддиректории и зависимые файлы путем рекурсии. else "Если это файл let name = s:cmake_project_indent(a:level) . pair[0] call append(line('$'), name) endif endfor endfunction
Привяжем функцию s:cmake_project_window() к команде CMakePro
command -nargs=0 -bar CMakePro call s:cmake_project_window()
Также нам необходима команда для генерации cmake файлов.
command -nargs=1 -bar CMake call s:cmake_project_cmake(<f-args>) function! s:cmake_project_cmake(srcdir) if !isdirectory(a:srcdir) echo "This directory not exists!" . a:srcdir return endif let s:cmake_project_dir = a:srcdir exec "cd" a:srcdir if !isdirectory(g:cmake_project_build_dir) call mkdir(g:cmake_project_build_dir) endif cd build exec "!cmake" "../" cd .. call s:cmake_project_window() endfunction
При движении курсора по панели, должен открываться файл под курсором. Создадим функцию s:cmake_project_cursor_moved() и привяжем ее к сигналу CursorMoved.
autocmd CursorMoved * call s:cmake_project_cursor_moved()
Для того, чтобы функция работала только с буфером панели, проверяем его имя перед выполнением.
function! s:cmake_project_cursor_moved() if exists('s:cmake_project_bufname') && bufname('%') == s:cmake_project_bufname <code> endif endfunction
Получаем данные текущей строки и выделяем слово под курсором.
let cmake_project_filename = getline('.') let fullpath = s:cmake_project_var(cmake_project_filename) let highlight_pattern = substitute(fullpath, '[.]', '\\.', '') let highlight_pattern = substitute(highlight_pattern, '[/]', '\\/', '') exec "match" "ErrorMsg /" . highlight_pattern . "/"
Определим директорию, в которой находится файл через отступы. Если файл является элементом n-того уровня, то директория, в которой находится файл является ближайщий элемент сверху с отступами n-1-ого уровня.
let level = s:cmake_project_level(cmake_project_filename) let level -= 1 let finding_line = s:cmake_project_find_parent(level, line('.')) while level > -1 let path = s:cmake_project_var(getline(finding_line)) let fullpath = path . fullpath let level -= 1 let finding_line = s:cmake_project_find_parent(level, finding_line) endwhile let fullpath = "/" . fullpath "формируем путь путем конкатенации элементов
Открываем необходимый файл
if filereadable(fullpath) wincmd l exec 'e' fullpath setf cpp endif endif
В итоге получилось:

Исходники брать здесь: