23

Let's assume I am editing some Python code in vim that looks a bit like:

myobj.myfunc("Some string parameter that goes on and on and on and on and sometimes doesn't"
             "split very"
             "neatly over different lines so that"
             "it is formatted attractively")

I would prefer to reformat this so it reflows up to the textwidth I have set:

myobj.myfunc("Some string parameter that goes on and on and "
             "on and on and sometimes doesn't split very "
             "neatly over different lines so that it is "
             "formatted attractively")

Is there an easy way to do this? If this were a conventional text paragraph, gqip or similar would be useful, but this won't handle the quotes used to delinate the string.

Note: I am asking specifically about Python here, but ideally, this answer would be relevant for many types of programming language that allow string continuation.

  • 1
    Similar question here: vim.1045645.n5.nabble.com/… – Andrew Ferrier Feb 17 '15 at 10:30
  • 3
    Have you also considered rewriting the code as a """multi-line string"""? – 200_success Feb 19 '15 at 7:18
  • 1
    @200_success, that's a great tip, thanks. I didn't know about that feature of Python at all. It seems to work, mostly - although all parameters to the function are reflowed, which isn't ideal. I think my question is still valid for the type of string I have though, and certainly for other langs. – Andrew Ferrier Feb 19 '15 at 9:40
  • 2
    Note that a multi-line string will add whitespace characters, which may be a problem if whitespace matters in the string. – Matt Boehm Feb 23 '15 at 20:41
  • Very related question: vi.stackexchange.com/questions/2135/… ... – Martin Tournoij Mar 15 '15 at 6:58
6

Oh that's a tough one. I think the best approach is possibly a macro but more likely a plugin. Here is an example I whipped up (I need a better hobby). It seemed to work for me but would need the python indenting plugin to indent properly. Try it out.

function ReformatMultiLines()
  let brx = '^\s*"'
  let erx = '"\s*$'
  let fullrx = brx . '\(.\+\)' . erx
  let startLine = line(".")
  let endLine   = line(".")
  while getline(startLine) =~ fullrx
    let startLine -= 1
  endwhile
  if getline(endLine) =~ erx
    let endLine += 1
  endif
  while getline(endLine) =~ fullrx
    let endLine += 1
  endwhile
  if startLine != endLine
    exec endLine . ' s/' . brx . '//'
    exec startLine . ' s/' . erx . '//'
    exec startLine . ',' . endLine . ' s/' . fullrx . '/\1/'
    exec startLine . ',' . endLine . ' join'
  endif
  exec startLine
  let orig_tw = &tw
  if &tw == 0
    let &tw = &columns
    if &tw > 79
      let &tw = 79
    endif
  endif
  let &tw -= 3 " Adjust for missing quotes and space characters
  exec "normal A%-%\<Esc>gqq"
  let &tw = orig_tw
  let endLine = search("%-%$")
  exec endLine . ' s/%-%$//'
  if startLine == endLine
    return
  endif
  exec endLine
  exec 'normal I"'
  exec startLine
  exec 'normal A "'
  if endLine - startLine == 1
    return
  endif
  let startLine += 1
  while startLine != endLine
    exec startLine
    exec 'normal I"'
    exec 'normal A "'
    let startLine += 1
  endwhile
endfunction

Then you could use it with :call ReformatMultiLines() and/or use it in a mapping.

6

If this is a regular occurrence, you'd be best off looking for a plugin or using @Sukima 's function. If I were doing this on the fly however, here's what I'd probably do:

  1. Add a newline after the opening paren and before the closing paren so that strings are on separate lines.

    myobj.myfunc(
                 "Some string parameter that goes on and on and on and on and sometimes doesn't"
                 "split very"
                 "neatly over different lines so that"
                 "it is formatted attractively"
    )
    
  2. Select lines with strings and delete surrounding quotes: :norm ^x$x

  3. Reduce textwidth (to account for missing quotes) :set tw-=2
  4. Re-select and format: gvgq
  5. Fix textwidth: :set tw+=2
  6. Re-add quotes: gv:norm I"<Esc>A". Instead of <Esc> you want to insert a literal escape by typing ctrl-v followed by the escape key. Since I map jj to escape, I usually just type jj here.
  7. Optionally use J to re-join the first two/last two lines. When I have a very long string in python like this, I typically prefer to start it on the next line and only indent it one level more than the previous line. This gives more horizontal space for the string and just feels more natural to me. Alternatively, you could save the string to a variable somewhere above. Assuming it's a static string, you could even save it to a global variable so that it's at a much lower indentation level and can fit on fewer lines. Note that you are to rejoin the closing paren on the same line, you probably want to decrement/increment textwidth by 3 instead of 2.
  • 1
    Thanks. I just tried this but :set tw-=2 results in E487: Argument must be positive: tw-=2. Is the syntax incorrect? – Andrew Ferrier Mar 23 '15 at 11:30
  • 1
    Check the current value of textwidth with :set tw?. My guess is that you have textwidth set to 0. These steps assume that textwidth starts out as the number of characters you want per line. – Matt Boehm Mar 23 '15 at 14:05
  • In your example you probably want to s/2/3/ and add a space before the closing quote, your long string would not have proper spacing between words. – cdosborn Sep 24 '18 at 18:28
1

Parsing and formatting code is hard, especially for dynamic languages such as Python.

Instead of trying to parse Python in Vimscript, I recommend you can use an external formatter such as yapf. With my answer here you can write this automatically on write with:

augroup write_cmd
    autocmd BufWritePre *.py call s:write_cmd('yapf')
augroup end

The s:write_cmd() function is in the other answer – I won't add it here so I won't have to update it in two places if I find a bug :-)

You can also set formatprg to yapf and manually format it with gq.

  • Hi. Helpful answer, but I'm downvoting as I don't think it answers this question: this question is very specifically asking about reformatting multiline strings, which yapf appears not to do. Nevertheless I agree it looks like a useful tool. – Andrew Ferrier Jan 7 '17 at 14:28
  • @AndrewFerrier Fair enough; I could of sworn I saw it reformat strings when I tested this the other day though, but can't get it to work now either :-/ Guess I was confused. – Martin Tournoij Jan 8 '17 at 16:36
0

The following functions and corresponding mappings solved this problem for me:

function! BreakMultlineString(quote)
let c = 100
while c == 100
    normal 100|
    let c = virtcol('.')
    if c == 100
        execute "normal ," . a:quote
    endif
endwhile
endfunction

function! JoinMultilineString()
    let c = "'"
    while c == "'" || c == '"'
        normal gj^
        let c = matchstr(getline('.'), '\%' . col('.') . 'c.')
        normal k
        if c == "'" || c == '"'
            normal ,j
        endif
    endwhile
endfunction

" break lines
nnoremap <Leader>" 100\|Bi"<CR>"<Esc>
nnoremap <Leader><Leader>" :call BreakMultlineString('"')<CR>
nnoremap <Leader>' 100\|Bi'<CR>'<Esc>
nnoremap <Leader><Leader>' :call BreakMultlineString("'")<CR>

" join lines
nmap <Leader>j Jds'ds"x
nnoremap <Leader>J :call JoinMultilineString()<CR>

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.