Первое, что нужно упомянуть здесь, это то, что для больших файлов эта функция будет совершенно непрактичной. Причина в том, что строка состояния перерисовывается после каждого перемещения курсора после завершения каждой команды и, вероятно, после других событий, о которых я даже не подозреваю. Выполнение поиска по регулярному выражению во всем буфере и, кроме того, не только текущий буфер, но и каждое видимое окно (поскольку каждое окно имеет свою собственную строку состояния), значительно замедлит работу. Не поймите меня неправильно; идея за этой функцией является хорошей, так как это даст вам немедленную и полностью автоматическую индикацию вашей оставшейся работы, но компьютеры просто не бесконечно эффективны (к сожалению), и поэтому это может легко стать проблемой. Я редактировал файлы с миллионами строк текста, и один поиск регулярных выражений может занять много секунд на таких буферах.
Но при условии, что ваши файлы останутся довольно маленькими, я выяснил три возможных решения, с помощью которых вы можете достичь этого.
Решение # 1: ех: s и перенаправить выход
Вы можете использовать :exe
из функции для выполнения команды :s
с параметризованным рисунком, и :redir
перенаправить вывод в локальный переменный.
К сожалению, это имеет две нежелательных побочные эффектов, которые, в контексте этой функции, будут полные обшивочными выключатели, так как они будут происходить каждый раз, когда строка состояния перерисовывается:
- Курсор находится переместился в начало текущей строки.(Личные заметки: я никогда не понимал, почему vim делает это, независимо от того, используете ли вы
:s
из вызова строки состояния или вручную печатаете его в командной строке vim.)
- Визуальный выбор, если таковой имеется, потерял.
(И на самом деле может быть более неблагоприятными последствиями, что я не в курсе.)
Проблема курсора может быть исправлен путем сохранения и восстановления позиции курсора с помощью 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
Помимо решения моей проблемы, это был самый тонкий, самый подробный и образовательный ответ, который я могу вспомнить. Благодарим вас за предоставление опций, четких объяснений, плюсов и минусов в дополнение к законному рабочему решению с комментарием code_. Благодарю вас, сэр. ** Форум плакатов мира, смотреть и учиться! ** –
samusugiru