Сегодня поговорим о создании дополнений для 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
В итоге получилось:
Исходники брать здесь: