4

I'm writing my own indentexpr to indent my code, the indentexpr is run in a sandbox, so one important point is that I cannot change the text content.

I have set up the syntax highlighting for quoted strings, how do I know the cursor is at the boundary " in this case?

"A""B"
  ^^

Two strings "A" and "B" stick together. The API synIDattr() can let me know the name of the syntax item, say "myString", but cannot tell the start and end of this item.

I cannot simply move the cursor from "A" to "B" (or vise versa) and say I find the last quote of "A".

Is there an easy way to tell the difference between "A" and "B" without manually counting quotes (for performance)?


The example above is made up for easier comprehension. The real use case is about multiline strings. For example:

var s = "A
"
"B".shout()

s = """A
"""
"B".shout()
  • You could do a "search for boundaries" function that check the id under the cursor and move left-right until this id change, this will give you the boundaries. – nobe4 May 19 '16 at 10:39
  • @Nobe4 synID() gives the same id for both "A" and "B". – V.D.D May 19 '16 at 10:51
  • because they are both strings? It's normal. I'm not sure I understand your question here... – nobe4 May 19 '16 at 10:58
  • 1
    @Nobe4 Edited. Two strings stick together, so there is no non-string characters in the middle of them to tell the boundary. – V.D.D May 19 '16 at 11:32
  • 1
    @TommyA Sorry. I've added back the real code. Two strings can be on different lines. And they can span multiple lines. – V.D.D May 19 '16 at 13:29
3

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.

  • Ah, I have to count the quote with regexps. Thanks! – V.D.D May 20 '16 at 7:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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