This is a problem I encountered in my plugin: braceless.vim. You won't be able to do this using only syntax groups. Some syntaxes make a distinction between strings and heredoc/docstring, but you will still face the same problem if those are sitting next to each other. Here is the relevant snippet from braceless.vim:
let s:docstr = '\%("""\|''''''\)'
let s:syn_string = '\%(String\|Heredoc\|DoctestValue\|DocTest\|DocTest2\|BytesEscape\)$'
" Test if the line is a string. Accepts a column number to test. If no
" column number is provided, test the first and last column of the line.
function! s:is_string(line, ...)
return synIDattr(synID(a:line, a:0 ? a:1 : col([a:line, '$']) - 1, 1), 'name') =~? s:syn_string
\ && (a:0 || synIDattr(synID(a:line, 1, 1), 'name') =~? s:syn_string)
endfunction
" Returns the start and end lines for docstrings. Accepts a second argument
" to limit the search.
function! s:docstring(line, ...)
let l = nextnonblank(a:line)
let doc_head = 0
let doc_tail = 0
let bounds = a:0 ? a:1 : [1, line('$')]
while l >= bounds[0]
if getline(l) =~ s:docstr && s:is_string(nextnonblank(l + 1))
\ && !s:is_string(prevnonblank(l - 1))
let doc_head = l
break
elseif !s:is_string(l)
break
endif
let l = prevnonblank(l - 1)
endwhile
if doc_head == 0
return [0, 0]
endif
let l = prevnonblank(a:line)
while l <= bounds[1]
if getline(l) =~ s:docstr && s:is_string(prevnonblank(l - 1))
\ && !s:is_string(nextnonblank(l + 1))
let doc_tail = l
break
elseif !s:is_string(l)
break
endif
let l = nextnonblank(l + 1)
endwhile
return [doc_head, doc_tail]
endfunction
s:docstring()
starts by searching backwards from the supplied line number. If it finds a matching docstring delimiter on the current line, it tests the previous line to see if it's not a string that spans an entire line. This makes up for cases where a double quoted docstring might have single quote docstring delimiters in it ("""a\n'''\nb"""
). It otherwise stops searching on the first non-blank non-string line encountered. If it finds the beginning delimiter, it continues the search in the opposite direction from the supplied line number. The speed of this function depends on how large the docstring block is.
In this case:
s = """A
"""
"B".shout()
s:docstring()
could determine that "B"
is not part of the docstring above it. But, for:
var s = "A
"
"B".shout()
You would have to adapt the function to search for standard string delimiters, and search for standard string boundaries after you have determined that the current line is not in a docstring.
For my plugin's needs, I considered contiguous docstrings to be an entire block since they would indent to the same level. If you still want to make a distinction between:
"""
A
"""
"""
B
"""
You could change the following line:
if getline(l) =~ s:docstr && s:is_string(nextnonblank(l + 1))
\ && (!s:is_string(prevnonblank(l - 1)) || getline(prevnonblank(l - 1)) !~ s:docstr)
That way, if the previous non-blank line is not a string or another docstring delimiter is encountered, it would take the current line as the beginning of the docstring. But, this would break the case cited above for mixed docstring delimiters ("""a\n'''\nb"""
).
For the sake of speed in indentexpr
, the function could be modified to only search backwards, since indentation depends the lines above the current line.
synID()
gives the same id for both"A"
and"B"
.