For best experience, I want to enable some snippets to be expanded only in Tex Math environments. A wonderful person, lacygoill helped me on GitHub to achieve just this with the following setup:

Let the context function and a snippet using the context be given by:

global !p
def isMath():
    return int(vim.eval("get(reverse(map(synstack(line('.'), col('.')), {i,v -> synIDattr(v, 'name')})), 0, '') =~# 'texMathZone\%(X\|Y\|V\)'"))

context "isMath()"
snippet pow "" iA

Now isMath above doesn't work on cases when there's no space inbetween the math delimiters such as $|$ or \(|\), it only works on cases like $ | $ or $\( | \)$, so the other version of isMath that is derived is:

def isMath():
    return int(vim.eval("get(reverse(map(synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0)), {i,v -> synIDattr(v, 'name')})), 0, '') =~# 'texMathZone\%(X\|Y\)'"))

Both seem to work 99% of the time. There's one time that they doesn't work which is important too. Consider the following tex code: $e|$, type pow to expand to e^{|}, now type x followed by pow. I expect it to expand to e^{x^{|}}, instead it doesn't expand pow anymore, it stays as e^{xpow}. (all the expansions should happen automatically because of A and | stands for cursor position).

The second variation of isMath fails in an additional case too, consider the following snippet:

snippet __ "subscript" iA

This will not expand in $|$ or $ | $ at all.

How can the two python functions above for context be adjusted to consider the nested curly brace {} cases? I don't understand why this is happening at all either, because even in e{|} position, the cursor is located in texMathZoneV syntax group. I'd prefer to see an adjustment made to the second isMath because it's convenient to expand in $|$ where there are no spaces inbetween $$. However, if that's impossible to achieve, it's not a problem to write $ | $ either so adjustments to the first isMath are very welcome too.

To sum up, I'm looking for a function that correctly identifies it is in math environment and as lacygoill suggested it's a good idea to use syntax groups as they're always right. The isMath() functions above have been doing it well so far, with the nested {} being an exception as described for both functions and with $__$ not expanding at all for the second function. A new function with a different approach is also welcome that'd be working as expected, I think there are more issues than meet the eye with the current ones. If anyone's been through this, I'd really appreciate a hand!


texMathTextZones = ['texMathText']
texMathTextZoneIds = vim.eval('map('+str(texMathTextZones )+", 'hlID(v:val)')")
return not set(texMathZoneIds).isdisjoint(synstackids) and set(texMathTextZoneIds).isdisjoint(synstackids)

I added these lines to ignore some scopes. Is there a better way to do it?

2 Answers 2



I noticed that checking simply for texMathZone... at the beginning or end of the synstack is not sufficient. Every element has to be checked.

As the OP has requested there should be a possibility to disable the autoexpansion for \text{...} within math zones (updated update). This is done by checking first if the Id of a texIgnoreMathZones matches.

global !p
texMathZones = ['texMathZone'+x for x in ['A', 'AS', 'B', 'BS', 'C', 'CS', 'D', 'DS', 'V', 'W', 'X', 'Y', 'Z']]
# If you have http://www.drchip.org/astronaut/vim/vbafiles/amsmath.vba.gz,
# you want to add:
# texMathZones += ['texMathZone'+x for x in ['E', 'ES', 'F', 'FS', 'G', 'GS', 'H', 'HS', 'I', 'IS', 'J', 'JS', 'K', 'KS', 'L', 'LS']]
texIgnoreMathZones = ['texMathText']

texMathZoneIds = vim.eval('map('+str(texMathZones)+", 'hlID(v:val)')")
texIgnoreMathZoneIds = vim.eval('map('+str(texIgnoreMathZones)+", 'hlID(v:val)')")

def isMath():
    synstackids = vim.eval("synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0))")
    if not set(texIgnoreMathZoneIds).isdisjoint(synstackids):
        return False
    return not set(texMathZoneIds).isdisjoint(synstackids)
    # isdisjoint() can short-circuit as soon as a match is found


Note you could remove a few letters when you follow https://ctan.org/pkg/l2tabu-english. This means that you do not use eqnarray and $$ ... $$ which are the letters B, BS, and Y.


I did a little bit of debugging of your code by adding a few print statements to see what is going on. I have considered your second isMath() function which takes a column offset of -1 if the cursor is not in the first column.

The code with print statements looks like following which contains a modification: the call of reverse() has been removed when the value for syntaxgroup is eventually determined. Both syntaxgroups (with or without reverse()), however, are printed:

global !p
def isMath():
    synstack = vim.eval("map(synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0)), {i,v -> synIDattr(v, 'name')})")
    print("synstack: " + str(synstack))
    syntaxgroup = vim.eval("get(reverse(map(synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0)), {i,v -> synIDattr(v, 'name')})), 0, '')")
    print("syntaxgroup (rev): "+ syntaxgroup)
    # MODIFICATION reverse() is not necessary since texMathZone is the outer group anyway.
    syntaxgroup = vim.eval("get(map(synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0)), {i,v -> synIDattr(v, 'name')}), 0, '')")
    print("syntaxgroup: "+ syntaxgroup)
    return int(vim.eval("'"+syntaxgroup+"' =~# 'texMathZone\%(X\|Y\|V\)'"))

context "isMath()"
snippet ^^ "^{ }" iA

context "isMath()"
snippet __ "_{ }" iA

snippet $$ "$ $" iA

I have used ^^ instead of pow as trigger word for superscript as personal preference. Additionally, there is the snippet $$ for starting an inline math expression.

When I enter following sequence of characters in a tex file

  1. $$
  2. __,

I get in my buffer


The messages generated by the print statements are

enter image description here

What I am suprised about is, that the function isMath() is also triggered after pressing $.

Ok, the reason for this is clear: the autotrigger feature requires this:

After snippet is defined as being autotriggered, snippet condition will be checked on every typed character and if condition matches, then the snippet will be triggered.

I have also tested the modification for math environments such as equation and it also works.

IMHO your approach is good and I could not suggest anything better. If someone encounters a delay when fast typing, I would recommend to remove A to disable the autotrigger.

  • Hi. Unfortunately I don't have 15 reps to upvote your post. I'll try to test it thoroughly soon. Thank you very much! You mentioned that every element has to be checked, does this have performance/cost implications? Jan 8, 2019 at 20:11
  • I tried to find a fast solution by checking stackoverflow.com/questions/3170055/…. I think the list of ids (ints) of the texMathZones can be around 30-40 (e.g. when adding amsmath.vba.gz), the synstack list is probably less than 20. I think finding the intersection should be fast enough even it is performed for every key press.
    – Hotschke
    Jan 8, 2019 at 20:17
  • So far it has been doing very good and I don't notice slowdowns performance-wise either. I'm not using amsmath scopes though. Thank you very much for this again. I'm interested to see if someone comes up with a novel solution or sees a way to minimize the cost for identifying math scopes. Jan 8, 2019 at 22:17
  • Can an additional feature be added such that it is never executed in texMathText even if in a math environment? This looks something like this $should return true here \text{but no longer true here}$ Jan 9, 2019 at 2:08
  • I updated my post with the code I added to ignore math environment in \text{}. I'm using isdisjoint again in case there are more texMathText zones that i'll need to ignore. What do you think about it? Jan 9, 2019 at 5:50

The syntax group based approach no longer works with the recent updates of vimtex (since around Dec 2020). One should use vimtex#syntax#in_mathzone() instead:

global !p
def isMath():
    return int(vim.eval('vimtex#syntax#in_mathzone()')) > 0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.