Main Ideas
There are 3 cases to consider (reworded slightly from the original question):
a) If not in an itemize environment, create one, and place the cursor after the first \item.
b) If in an itemize environment and if the list item is not empty, create a new \item.
c) If in an itemize environment and if the list item is empty, delete the line and jump out of itemize environment.
Instead of a single snippet, we define three snippets that address each of the cases separately:
# case a
context "not in_itemize(snip)"
snippet \item "start list of items" b
\begin{itemize}
\item $1
\end{itemize}
endsnippet
# case b
context "in_itemize(snip)"
snippet "(^\s*)\\item\s*\S+.*$" "create next item" r
`!p snip.rv=match.group(0)`
`!p snip.rv=match.group(1)`\item $0
endsnippet
# case c
context "in_itemize(snip)"
post_jump "delline_exititemize(snip)"
snippet "^\s*\\item\s*" "exit itemize" r
endsnippet
We use a context in_itemize()
to check if we are in an itemize environment.
For case c, we have used the post_jump action delline_exititemize()
to manipulate the text buffer and cursor.
We use different regex snippets to distinguish cases b and c.
Snippet Details
Case a is the 'standard' snippet. It creates the itemize environment and places the cursor after \item.
Now we would like to distinguish between cases b and c. Instead of checking the tabstop's value, another way is to check if there are non-whitespace characters after \item (case b) or not (case c). This is done by a regular expression trigger, as seen by the 'r' at the end of the snippet header line. The regex \S+
in case b checks if there are non-whitespace characters.
For case b, the line
`!p snip.rv=match.group(0)`
repeats the original text, and
`!p snip.rv=match.group(1)`\item $0
adds a new \item to the list.
match.group(1)
captures the indentation of the previous line. This ensures that indentation is preserved.
For case c, the snippet doesn't produce any actual text. Rather, the post_jump action delline_exititemize
manipulates the buffer and cursor positions, by removing the current line, and moving the cursor outside the itemize environment.
Helpers for 'context' and 'post_jump'
We will have to write the two helper functions in_itemize()
and delline_exititemize()
in python (more precisely, using vim's python interface).
The code for the helper function in_itemize()
is as follows:
def in_itemize(snip):
# find using searchpairpos('\begin{itemize}','','\end{itemize}','bnWz')
beginpos = vim.eval("searchpairpos('\\\\begin{itemize}','','\\\\end{itemize}','bnWz')")
endpos = vim.eval("searchpairpos('\\\\begin{itemize}','','\\\\end{itemize}', 'nWz')")
curpos = snip.cursor.to_vim_cursor()
if beginpos == ['0','0'] or endpos == ['0','0']: return False
if posCompare(beginpos, curpos) < 0 and posCompare(curpos, endpos) < 0:
return True
else:
return False
This uses vim's searchpairpos()
function to find the locations of \begin{itemize} and \end{itemize}. It is wrapped with vim.eval()
since we are calling the vim function in python. These start and end positions are then compared with the cursor's position.
In short, this function checks if the cursor is encompassed by \begin{itemize} and \end{itemize}.
(The function posCompare
is rather straightforward to implement. It is included in the full code below)
The code for deleting the current line and exiting the itemize environment is as follows:
def delline_exititemize(snip):
cursorline = snip.cursor[0]
del snip.buffer[cursorline]
# add blank line after \end{itemize}
snip.buffer.append("\n", cursorline + 1 + 1)
# cursorline+1 is the linenr (1-based)
# place cursor on blank line after \end{itemize}
snip.cursor.set(cursorline+1, 0)
This deletes the current line, adds a new line after \end{itemize}, and moves the cursor to the desired location. We have used the properties of snip
to achieve this. See :h UltiSnips-custom-context-snippets
paragraph 6, for details. Note that we have to convert between 0-based and 1-based indexing.
Putting all pieces together
We have now filled in all parts. The last step is to place all python code in a global !p ... endglobal
block, or in a separate file in .vim/pythonx. For the former approach, here is the full code:
# helpers
global !p
def delline_exititemize(snip):
cursorline = snip.cursor[0]
del snip.buffer[cursorline]
# add blank line after \end{itemize}
snip.buffer.append("\n", cursorline + 1 + 1)
# cursorline+1 is the linenr (1-based)
# place cursor on blank line after \end{itemize}
snip.cursor.set(cursorline+1, 0)
def in_itemize(snip):
# find using searchpairpos('\begin{itemize}','','\end{itemize}','bnWz')
beginpos = vim.eval("searchpairpos('\\\\begin{itemize}','','\\\\end{itemize}','bnWz')")
endpos = vim.eval("searchpairpos('\\\\begin{itemize}','','\\\\end{itemize}', 'nWz')")
curpos = snip.cursor.to_vim_cursor()
if beginpos == ['0','0'] or endpos == ['0','0']: return False
if posCompare(beginpos, curpos) < 0 and posCompare(curpos, endpos) < 0:
return True
else:
return False
def posCompare(cur1, cur2):
""" returns -1 if cur1 is before cur2, +1 if after, and 0 if cur1==cur2
cur1, cur2 are required to be lists of the form [row,col]
"""
cur1r = int(cur1[0])
cur1c = int(cur1[1])
cur2r = int(cur2[0])
cur2c = int(cur2[1])
if cur1r < cur2r: return -1
elif cur1r > cur2r: return 1
else:
if cur1c < cur2c: return -1
elif cur1c > cur2c: return 1
else: return 0
endglobal
# snippets
context "not in_itemize(snip)"
snippet \item "start list of items" b
\begin{itemize}
\item $1
\end{itemize}
endsnippet
context "in_itemize(snip)"
snippet "(^\s*)\\item\s*\S+.*$" "create next item" r
`!p snip.rv=match.group(0)`
`!p snip.rv=match.group(1)`\item $0
endsnippet
context "in_itemize(snip)"
post_jump "delline_exititemize(snip)"
snippet "^\s*\\item\s*" "exit itemize" r
endsnippet