2015-06-11 2 views
3

Я искал это для этого и не смог найти то, что мне нужно.Vim Statusline: Поиск по словам

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

:%s/^I^I//n 

ВИМ возвращается: 16 матчей на 16 линиях

FYI Пояснение: Я работаю в файле CSV. Я ищу два символа табуляции (^ I^I), потому что это указывает на строки, над которыми мне все еще нужно работать. Таким образом, моя желаемая статусная линия будет указывать, сколько работы осталось в текущем файле.

Я не знаю, как ввести команду vim в статусную линию, я знаю, что% {} можно использовать для запуска функции, но как мне запустить команду поиска vim? Я пробовал варианты следующего, но они явно не правы и просто заканчиваются ошибкой.

:set statusline+= %{s/^I^I//n} 

Помогите мне vimy one kenobi, вы - моя единственная надежда!

ответ

3

Первое, что нужно упомянуть здесь, это то, что для больших файлов эта функция будет совершенно непрактичной. Причина в том, что строка состояния перерисовывается после каждого перемещения курсора после завершения каждой команды и, вероятно, после других событий, о которых я даже не подозреваю. Выполнение поиска по регулярному выражению во всем буфере и, кроме того, не только текущий буфер, но и каждое видимое окно (поскольку каждое окно имеет свою собственную строку состояния), значительно замедлит работу. Не поймите меня неправильно; идея за этой функцией является хорошей, так как это даст вам немедленную и полностью автоматическую индикацию вашей оставшейся работы, но компьютеры просто не бесконечно эффективны (к сожалению), и поэтому это может легко стать проблемой. Я редактировал файлы с миллионами строк текста, и один поиск регулярных выражений может занять много секунд на таких буферах.

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

Решение # 1: ех: s и перенаправить выход

Вы можете использовать :exe из функции для выполнения команды :s с параметризованным рисунком, и :redir перенаправить вывод в локальный переменный.

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

  1. Курсор находится переместился в начало текущей строки.(Личные заметки: я никогда не понимал, почему vim делает это, независимо от того, используете ли вы :s из вызова строки состояния или вручную печатаете его в командной строке vim.)
  2. Визуальный выбор, если таковой имеется, потерял.

(И на самом деле может быть более неблагоприятными последствиями, что я не в курсе.)

Проблема курсора может быть исправлен путем сохранения и восстановления позиции курсора с помощью getcurpos() и setpos(). Обратите внимание, что это должно быть getcurpos(), а не getpos(), потому что последнее не возвращает поле curswant, которое необходимо для сохранения столбца, в котором находится «хочет» курсор, который может отличаться от столбца, на котором находится «на самом деле» курсор (например, если курсор был перемещен в более короткую строку). К сожалению, getcurpos() является довольно недавним дополнением к vim, а именно 7.4.313, и на основе моего тестирования даже не работает корректно. К счастью, есть более старые функции winsaveview() и winrestview(), которые могут выполнять задачу идеально и совместимо. Поэтому пока мы будем использовать их.

Решение # 1a: Восстановление визуальный выбор с ГВ

Визуальный вопрос выбора я подумал может быть решена путем запуска gv в нормальном режиме, но по какой-то причине визуальный выбор становится полностью поврежден, делая это , Я протестировал это на Cygwin CLI и Windows gvim, и у меня нет решения для этого (в отношении восстановления визуального выбора).

В любом случае, вот результат выше конструкции:

fun! MatchCount(pat,...) 
    "" return the number of matches for pat in the active buffer, by executing an :s call and redirecting the output to a local variable 
    "" saves and restores both the cursor position and the visual selection, which are clobbered by the :s call, although the latter restoration doesn't work very well for some reason as of vim-7.4.729 
    "" supports global matching (/g flag) by taking an optional second argument appended to :s flags 
    if (a:0 > 1)| throw 'too many arguments'| endif 
    let flags = a:0 == 1 ? a:000[0] : '' 
    let mode = mode() 
    let pos = winsaveview() 
    redir => output| sil exe '%s/'.a:pat.'//ne'.flags| redir END 
    call winrestview(pos) 
    if (mode == 'v' || mode == 'V' || mode == nr2char(22)) 
     exe 'norm!gv' 
    endif 
    if (match(output,'Pattern not found') != -1) 
     return 0 
    else 
     return str2nr(substitute(output,'^[\s\n]*\(\d\+\).*','\1','')) 
    endif 
    return 
endfun 

set statusline+=\ [%{MatchCount('\\t\\t')}] 

Несколько случайных нот:

  • Использование ^[\s\n]* в структуре добычи матч подсчета необходимо баррель через ведущий разрыв строки, который захватывается во время перенаправления (не уверен, почему это происходит). Альтернативой было бы пропустить любой символ до первой цифры с нежелезным множителем на атоме точки, то есть ^.\{-}.
  • Ускорение обратных косых черт в значении параметра statusline необходимо, так как интерполяция/удаление обратной косой черты происходит при разборе значения параметра. В общем случае строки с одиночными кавычками не вызывают интерполяцию/удаление обратной косой черты, и наша строка pat, после разбора, в конечном итоге становится конкатенацией непосредственно с строкой :s, переданной в :exe, поэтому в этих точках интерполяция/удаление обратной косой черты (по крайней мере, не ранее к оценке команды :s, когда происходит интерполяция обратной косой черты наших обратных косых черт , происходит, что мы и хотим). Я считаю, что это немного запутывает, поскольку внутри конструкции %{} вы ожидаете, что это будет нормальное безупречное выражение VimScript, но так оно и работает.
  • Я добавил флаг /e для команды :s. Это необходимо для обработки случая с нулевым совпадением. Обычно :s действительно выдает ошибку, если нулевые совпадения. Для вызова строки состояния это большая проблема, так как любая ошибка, возникающая при попытке перерисовать строку состояния, заставляет vim аннулировать параметр statusline в качестве защитной меры для предотвращения повторных ошибок.Первоначально я искал решения, связанные с ловушкой ошибки, например :try и :catch, но ничего не получилось; после появления ошибки в источнике vim установлен флаг (called_emsg), который мы не можем отменить, и поэтому statusline обречен на эту точку. К счастью, я обнаружил флаг /e, который предотвращает выброс ошибки вообще.

Решение # 1b: Dodge визуальный режим с буферным локальным кэшем

Я не был удовлетворен с визуальным вопросом выбора, поэтому я написал альтернативное решение. Это решение фактически избегает запуска поиска вообще, если визуальный режим действует, и вместо этого вытаскивает последний известный счетчик поиска из буфера-локального кеша. Я уверен, что это никогда не приведет к тому, что счетчик поиска станет устаревшим, потому что невозможно отредактировать буфер без отказа от визуального режима (я уверен ...).

Так что теперь функция MatchCount() не связывайтесь с визуальным режимом:

fun! MatchCount(pat,...) 
    if (a:0 > 1)| throw 'too many arguments'| endif 
    let flags = a:0 == 1 ? a:000[0] : '' 
    let pos = winsaveview() 
    redir => output| sil exe '%s/'.a:pat.'//ne'.flags| redir END 
    call winrestview(pos) 
    if (match(output,'Pattern not found') != -1) 
     return 0 
    else 
     return str2nr(substitute(output,'^[\s\n]*\(\d\+\).*','\1','')) 
    endif 
    return 
endfun 

И теперь мы должны эта вспомогательной функция «предикат», который говорит нам, когда это (не) безопасно запустить команду :s:

fun! IsVisualMode(mode) 
    return a:mode == 'v' || a:mode == 'V' || a:mode == nr2char(22) 
endfun 

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

fun! BufferCallCache(buf,callName,callArgs,callElseCache) 
    let callCache = getbufvar(a:buf,'callCache') 
    if (type(callCache) != type({})) 
     unlet callCache 
     let callCache = {} 
     call UnletBufVar(a:buf,'callCache') 
     call setbufvar(a:buf,'callCache',callCache) 
    endif 
    if (a:callElseCache) 
     let newValue = call(a:callName,a:callArgs) 
     if (!has_key(callCache,a:callName.':Args') || !has_key(callCache,a:callName.':Value')) 
      let callCache[a:callName.':Args'] = [] 
      let callCache[a:callName.':Value'] = [] 
     endif 
     let i = len(callCache[a:callName.':Args'])-1 
     while (i >= 0) 
      let args = callCache[a:callName.':Args'][i] 
      if (args == a:callArgs) 
       let callCache[a:callName.':Value'][i] = newValue 
       return newValue 
      endif 
      let i -= 1 
     endwhile 
     let callCache[a:callName.':Args'] += [a:callArgs] 
     let callCache[a:callName.':Value'] += [newValue] 
     return newValue 
    else 
     if (has_key(callCache,a:callName.':Args') && has_key(callCache,a:callName.':Value')) 
      let i = len(callCache[a:callName.':Args'])-1 
      while (i >= 0) 
       let args = callCache[a:callName.':Args'][i] 
       if (args == a:callArgs) 
        return callCache[a:callName.':Value'][i] 
       endif 
       let i -= 1 
      endwhile 
     endif 
     return '' 
    endif 
endfun 

За что нам нужна эта вспомогательная функция, которую я нашел somewhere on the Internet лет назад:

fun! UnletBufVar(bufExpr, varName) 
    "" source: <http://vim.1045645.n5.nabble.com/unlet-ing-variables-in-buffers-td5714912.html> 
    call filter(getbufvar(a:bufExpr,''), 'v:key != '''.a:varName.'''') 
endfun 

И, наконец, это то, как мы можем установить statusline:

set statusline+=\ [%{BufferCallCache('','MatchCount',['\\t\\t'],!IsVisualMode(mode()))}] 

Решение №2: Позвонить match() на каждой строке

Я подумал о другом возможном решении, которое на самом деле намного проще и, кажется, отлично подходит для не-огромных файлов, хотя оно связано с большим количеством циклов и обработки на уровне VimScript. Это в петлю над каждой строки в файле и вызвать match() на нем:

fun! MatchCount(pat) 
    "" return the number of matches for pat in the active buffer, by iterating over all lines and calling match() on them 
    "" does not support global matching (normally achieved with the /g flag on :s) 
    let i = line('$') 
    let c = 0 
    while (i >= 1) 
     let c += match(getline(i),a:pat) != -1 
     let i -= 1 
    endwhile 
    return c 
endfun 

set statusline+=\ [%{MatchCount('\\t\\t')}] 

Решение # 3: Call search()/searchpos() неоднократно

Я написал несколько слегка сложные функции для выполнения глобальных и построчно соответствия , построенный вокруг searchpos() и search(), соответственно. Я включил поддержку и для необязательной начальной и конечной границ.

fun! GlobalMatchCount(pat,...) 
    "" searches for pattern matches in the active buffer, with optional start and end [line,col] specifications 
    "" useful command-line for testing against last-used pattern within last-used visual selection: echo GlobalMatchCount(@/,getpos("'<")[1:2],getpos("'>")[1:2]) 
    if (a:0 > 2)| echoerr 'too many arguments for function: GlobalMatchCount()'| return| endif 
    let start = a:0 >= 1 ? a:000[0] : [1,1] 
    let end = a:0 >= 2 ? a:000[1] : [line('$'),2147483647] 
    "" validate args 
    if (type(start) != type([]) || len(start) != 2 || type(start[0]) != type(0) || type(start[1]) != type(0))| echoerr 'invalid type of argument: start'| return| endif 
    if (type(end) != type([]) || len(end) != 2 || type(end[0]) != type(0) || type(end[1]) != type(0))| echoerr 'invalid type of argument: end'| return| endif 
    if (end[0] < start[0] || end[0] == start[0] && end[1] < start[1])| echoerr 'invalid arguments: end < start'| return| endif 
    "" allow degenerate case of end == start; just return zero immediately 
    if (end == start)| return [0,0]| endif 
    "" save current cursor position 
    let wsv = winsaveview() 
    "" set cursor position to start (defaults to start-of-buffer) 
    call setpos('.',[0,start[0],start[1],0]) 
    "" accumulate match count and line count in local vars 
    let matchCount = 0 
    let lineCount = 0 
    "" also must keep track of the last line number in which we found a match for lineCount 
    let lastMatchLine = 0 
    "" add one if a match exists right at start; must treat this case specially because the main loop must avoid matching at the cursor position 
    if (searchpos(a:pat,'cn',start[0])[1] == start[1]) 
     let matchCount += 1 
     let lineCount += 1 
     let lastMatchLine = 1 
    endif 
    "" keep searching until we hit end-of-buffer 
    let ret = searchpos(a:pat,'W') 
    while (ret[0] != 0) 
     "" break if the cursor is now at or past end; must do this prior to incrementing for most recent match, because if the match start is at or past end, it's not a valid match for the caller 
     if (ret[0] > end[0] || ret[0] == end[0] && ret[1] >= end[1]) 
      break 
     endif 
     let matchCount += 1 
     if (ret[0] != lastMatchLine) 
      let lineCount += 1 
      let lastMatchLine = ret[0] 
     endif 
     let ret = searchpos(a:pat,'W') 
    endwhile 
    "" restore original cursor position 
    call winrestview(wsv) 
    "" return result 
    return [matchCount,lineCount] 
endfun 

fun! LineMatchCount(pat,...) 
    "" searches for pattern matches in the active buffer, with optional start and end line number specifications 
    "" useful command-line for testing against last-used pattern within last-used visual selection: echo LineMatchCount(@/,getpos("'<")[1],getpos("'>")[1]) 
    if (a:0 > 2)| echoerr 'too many arguments for function: LineMatchCount()'| return| endif 
    let start = a:0 >= 1 ? a:000[0] : 1 
    let end = a:0 >= 2 ? a:000[1] : line('$') 
    "" validate args 
    if (type(start) != type(0))| echoerr 'invalid type of argument: start'| return| endif 
    if (type(end) != type(0))| echoerr 'invalid type of argument: end'| return| endif 
    if (end < start)| echoerr 'invalid arguments: end < start'| return| endif 
    "" save current cursor position 
    let wsv = winsaveview() 
    "" set cursor position to start (defaults to start-of-buffer) 
    call setpos('.',[0,start,1,0]) 
    "" accumulate line count in local var 
    let lineCount = 0 
    "" keep searching until we hit end-of-buffer 
    let ret = search(a:pat,'cW') 
    while (ret != 0) 
     "" break if the latest match was past end; must do this prior to incrementing lineCount for it, because if the match start is past end, it's not a valid match for the caller 
     if (ret > end) 
      break 
     endif 
     let lineCount += 1 
     "" always move the cursor to the start of the line following the latest match; also, break if we're already at end; otherwise next search would be unnecessary, and could get stuck in an infinite loop if end == line('$') 
     if (ret == end) 
      break 
     endif 
     call setpos('.',[0,ret+1,1,0]) 
     let ret = search(a:pat,'cW') 
    endwhile 
    "" restore original cursor position 
    call winrestview(wsv) 
    "" return result 
    return lineCount 
endfun 
+1

Помимо решения моей проблемы, это был самый тонкий, самый подробный и образовательный ответ, который я могу вспомнить. Благодарим вас за предоставление опций, четких объяснений, плюсов и минусов в дополнение к законному рабочему решению с комментарием code_. Благодарю вас, сэр. ** Форум плакатов мира, смотреть и учиться! ** samusugiru

0

Может быть не совсем то, что вы ищете, но если поставить функцию, как следующее в файле $ HOME/.vimrc файл, который вы могли бы сделать:

:set statusline+=%!SearchResults('^I^I') 

$ HOME/.vimrc

function SearchResults(q) 
    redir => matches 
    silent! execute "%s/".a:q."//n" 
    redir END 
    return substitute(matches, "^.", "", "") 
endfunction 

Если ничего другого, может быть, это приблизит вас.