Пишем плагин для поддержки cmake проектов под vim

в 22:03, , рубрики: cmake, perl, vim, метки: , ,

Сегодня поговорим о создании дополнений для VIM.

Недавно у меня возникла идея вкрутить в него поддержку 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

В итоге получилось:

image

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

Автор: Ignotus

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js