I am using vim to edit latex documents. Often, I will want to change the name of a variable to something else. However, it's a little annoying to have to wade through irrelevant matches when I search for the variable name. I would like to search only in "math zones" as defined in the tex syntax file. Is there an easy way to limit a search to a specific syntax region?
7 Answers
Here is a function that you could try. I've only tested it a little bit but it seems to work ok.
function! JJSyntaxSearch(pattern, syntaxitem)
while search(a:pattern, 'W') > 0
for id in synstack(line("."),col("."))
if synIDattr(id,"name") =~? a:syntaxitem
return line(".")
endif
endfor
endwhile
return 0
endfunc
command! -nargs=* JJSyntaxSearch call JJSyntaxSearch(<f-args>)
The function calls Vim's search function in a loop until it finds an 'ordinary' match that is also a 'qualified' match to a particular syntax item, or until it reaches the end of the buffer.
It takes two arguments. The first is the search pattern and it is passed on unchanged to Vim's search()
function. See :help search()
for how that function works. The second argument is the name of the syntax item on which you want to filter these matches. This can also be a pattern and is compared as a case-insensitive regular expression.
To find the pattern 'alpha' in math zones you can do
:JJSyntaxSearch alpha texMathZone.
The .
at the end is the pattern atom for any single character, see :help /.
. This is because there many math zone syntax items called texMathZoneA
, texMathZoneB
etc.
The search()
function moves the cursor to the next 'ordinary' match. Then our function gets all the syntax item IDs for that new cursor position with synstack()
. Looping through them, it retrieves the name of each item in turn and tests it against the syntaxitem
argument. See :help synstack()
and :help synIDattr()
. If there is a 'qualified' match it returns the line number. If it reaches the end of the buffer without finding any 'qualified' match it returns 0
. The point of the function is to get the cursor to the next 'qualified' match so you can do some operation there, but it is useful to return "line number or 0" to indicate whether it is meaningful to call the function again, for instance if you want to call the function repeatedly from another function or a macro.
I chose to use the W
flag for the search()
because it prevents 'wrapping around' the end of the buffer. Otherwise the function might get stuck as it keeps finding the same 'ordinary' matches but no 'qualified' matches.
-
Thanks for your help! I haven't tried this function yet, but it looks like what I wanted.– Rob F.Commented Aug 18, 2015 at 0:46
-
-
2Note: late Vim 8.2 supports
skip
expressions for thesearch()
function Commented Aug 26, 2020 at 14:53
This should perhaps be a comment but seems I have no reputation to do that. As an exercise in creating a vim-plugin I created one based on the answer of jjaderberg which you can find here: https://github.com/AckslD/vim-sisr
-
2
Since 8.2.0915, search()
supports a fifth optional argument which – among other types of expressions – can be a lambda.
Assuming you wanted to look for the literal string pat
, you could run:
:call search('pat', '', 0, 0, {-> synstack('.', col('.'))->map({_, v -> synIDattr(v, 'name')})->match('\ctexmathzone') == -1})
This can be turned into a custom command:
com -nargs=1 SearchInMathZone call s:SearchInMathZone(<q-args>)
fu s:SearchInMathZone(pat) abort
call search(a:pat, '', 0, 0, {->
"\ get the stack of syntax items under the cursor
\ synstack('.', col('.'))
"\ translate the IDs into names
\ ->map({_, v -> synIDattr(v, 'name')})
"\ skip the match if no syntax item under the cursor contains "texmathzone" in its name
\ ->match('\ctexmathzone') == -1})
endfu
The code gets a little more readable in Vim9 script, because the line continuations become implicit, and you can get rid of the leading backslashes:
vim9script
com -nargs=1 SearchInMathZone SearchInMathZone(<q-args>)
def SearchInMathZone(pat: string)
search(pat, '', 0, 0, () =>
# get the stack of syntax items under the cursor
synstack('.', col('.'))
# translate the IDs into names
->mapnew((_, v) => synIDattr(v, 'name'))
# skip the match if no syntax item under the cursor contains "texmathzone" in its name
->match('\ctexmathzone') == -1)
enddef
It requires a recent Vim version; it works on 8.2.2332. It should also be faster once the skip expression is compiled.
Note: you can always substitute line('.')
with '.'
whenever a function expects a line address, provided that the function can only work on the current buffer (not other buffers). synstack()
is one of them:
synstack(line('.'), col('.'))
⇔
synstack('.', col('.'))
For more info, see :h search()
.
Credit @Christian Brabandt for the reminder, and for writing the Vim patch making search()
support a skip expression.
In case your "math zones" are qualified by some syntactical pattern, say, the string "math", then you could at least use pattern sequences to find a line with the matching pattern, as in:
:/math//your pattern/
(Note: the two patterns may be on different lines!)
This may not restrict your matches to the zones - for that you'd need to identify the end of the zone -, but it will display only those matches after an initial pattern /math/. You can repeat the search for the next occurrence by using the ex-mode history (:
up-arrow
enter
).
Note: ISTR that in earlier vim/vi versions I was also able to use that method directly as a search command (as opposed to an ex-mode command)
/math//your pattern/
with the advantage of less typing and using of the n
and N
commands, but I cannot confirm that for my current vim version.
If you're searching for the string foo
inside $...$
or $$...$$
, this should work:
\$.*\zsfoo
This anchors the match to a $
character but only selects the string foo
.
It's not really an easy solution, but one could use a :global
+ :substitute
to find "math zones" and search inside them:
:global /\v\\begin\{%(align|alignat|displaymath)\}/ +1,/\\end/-1 s/foo/&/gc
Pretending that :substitute
is a search command, you can go to the next occurrence with n
and quit with q
. Should you confirm a substitution by mistake there shouldn't be any problem.
You will have to set the list inside %(
... )
to the names of the math environments you use (the full list is a bit hefty: align|alignat|displaymath|eqnarray|equation|flalign|gather|math|multline|subequations|xalignat|xxalignat
). Note that I'm using a very magic (\v
) pattern here.
Explanation:
:global
execute an Ex command (:s
) on each line where the pattern matches (:help :global
)
/\v
start a very magic pattern (:h /
, :h pattern
, :h magic
)
\\begin\{
match a literal \begin{
%(align|alignat|displaymath)
match any of these words (:h /\%(
, :h /bar
)
\}/
match a literal }
and end the pattern.
+1,/\\end/-1
a range consisting of the next line (counting from the one found by :global
) up to the line before the one found by the search pattern /\\end
. Within that range,
s/foo/&/gc
search-and-replace every occurrence of the literal foo
with itself, after confirmation (:h :substitute
)
-
This is certainly a start, but I make frequent use of the equation, align, and IEEEeqnarray environments, and I don't want to exclude those from my search.– Rob F.Commented Jul 28, 2015 at 18:23
-
@RobF.: it's quite possible to match inside a range of environments, but it's not going to be easy unless one scripts it. If, however, you consistently use
\mathit
around variable names, finding them is much easier. Commented Jul 28, 2015 at 18:33
If you're searching for the string 'morning' inside file
/morning
then enter and press n to move forward direction
-
1The question said
I would like to search only in "math zones"
your method searches for the whole file it doesn't delimit a research zone.– statoxCommented May 6, 2016 at 15:51
There is no option but to write a dedicated function for this. I wrote a function that searches the string you want and only stops if either:
- it runs out of matches
- there is a match which is inside math environment(it will stop on the match)
You just call it with the string you want to search.
function! Finmath(nextstr)
while 1
if search(a:nextstr)==0
break
endif
if searchpairpos('\(', '','\)','n')!=[0,0]
break
endif
if searchpairpos('\[', '','\]','n')!=[0,0]
break
endif
let a = getcurpos()
if count(strpart(getline('.'),0,a[2]),'$')%2==1
break
endif
endwhile
endfunction
I intend to write a similar function for GrammerousCheck that only stops on grammar errors outside math.
synIDattr()
to obtain syntax groups for the character under the cursor. You could write a function that performs the search (or doesnormal n
) and then usessynIDattr()
to decide whether to stay at this location, or continue the search forwards.