Aaron Sullivan
Aaron Sullivan

Reputation: 53

vim - How to Split single line Array literal into multiple lines?

Say I have this:

[ |"TypeScript", "HTML", "CSS", "SQL", "JavaScript" ]

Is there a quick way to bring me to this:

[ 
  |"TypeScript", 
  "HTML", 
  "CSS", 
  "SQL", 
  "JavaScript" 
]

I don't really care about where the cursor ends up, I just want to efficiently switch to/from single line to multi line.

The one solution I've found online is to do a search and replace. This hasn't helped me so far because the indenting rules are not respected. If I could find a search and replace technique that respected indenting I'd be happy to make a mapping and use that. So far the search and replace I've found looks like '<,'>s/,/,\r/g, and using the above example, would result in:

[ 
|"TypeScript", 
"HTML", 
"CSS", 
"SQL", 
"JavaScript" 
]

I know there's a way to do this because I've pressed whatever key combo this is by accident in the past and seen the desired effect, but not known what I accidentally pressed. It's possible it's in one of my plugins, so I've posted my plugins below.

My plugins:

'tpope/vim-commentary'
'tpope/vim-fugitive'
'tpope/vim-rhubarb'
'shumphrey/fugitive-gitlab.vim'
'terryma/vim-multiple-cursors'
'rhysd/committia.vim'
'tommcdo/vim-lion'
'elmcast/elm-vim'
'sheerun/vim-polyglot'
'tmsvg/pear-tree'
'tpope/vim-surround'
'Valloric/YouCompleteMe',
'ctrlpvim/ctrlp.vim',
'scrooloose/nerdtree',
'nathanaelkane/vim-indent-guides'
'fholgado/minibufexpl.vim'
'SirVer/ultisnips'
'honza/vim-snippets'

I'd also be happy to be exposed to another plugin that has the desired behavior.

Thanks!

Upvotes: 3

Views: 1040

Answers (2)

bert
bert

Reputation: 372

There is a plugin that does this: splitjoin.vim. It supports various filetypes, including javascipt and JSON.

From

[ |"TypeScript", "HTML", "CSS", "SQL", "JavaScript" ]

hit gS to get

[ 
  "TypeScript", 
  "HTML", 
  "CSS", 
  "SQL", 
  "JavaScript" 
]

To join it back into a single line, place the cursor on the first item and hit gJ.

Upvotes: 4

romainl
romainl

Reputation: 196556

Well, '<,'>s/,/,\r/g actually results in:

[ "TypeScript",
 "HTML",
 "CSS",
 "SQL",
 "JavaScript" ]

which is even farther from the desired result than what you posted.

If we stick with simple substitutions, the commas are irrelevant, here. It's the spaces that must be substituted:

:s/\s/\r/g

which results in:

[
"TypeScript",
"HTML",
"CSS",
"SQL",
"JavaScript"
]

with, the cursor on the ].

The next step is to reindent the lines you just changed:

='[

which gives you the desired result:

[
    "TypeScript",
    "HTML",
    "CSS",
    "SQL",
    "JavaScript"
]

See :help = and :help '[.

This is easy to map to something shorter:

nnoremap <key> :s/\s/\r/g<CR>='[

but it will do more harm than good in scenarios that vary, even slightly, from the one in your question.

What can we do to make this more generic?

First, instead of doing the substitution on a whole line, we can make a visual selection and restrict the substitution to that visual selection:

vi[                   " visually select what's between the brackets
:'<,'>s/\%V\s/\r/g    " substitute spaces contained in the visual
                      " selection with \r

This doesn't work, sadly, because the boundaries of \%V don't move with the text as we do our substitution. As soon as the first space is substituted, the rest goes one line below the selection and there is no space left to substitute in the visual area. Bummer.

Still keeping our substitutions short, we can work around that situation by going at it in two steps:

  1. substitute spaces with an exotic character that's unlikely to appear elsewhere in the line,

  2. substitute that fancy character with \r.

    vi[
    :'<,'>s/\%V\s/§/g
    :s/§/\r/g
    ='[
    

This works relatively well so we can change our previous mapping:

function! ExpandList()
    silent s/\%V\s/§/g
    silent s/§/\r/g
    silent normal ='[
endfunction
xnoremap <key> :<C-u>call ExpandList()<CR>
  • our normal mode mapping becomes a visual mode mapping,
  • instead of having a complicated RHS, we call a function that does the heavy lifting,
  • the function handles the three steps described earlier without messing with :help 'hlsearch',
  • the commands are silenced for improved comfort.

At this point, we can solve our problem by:

  1. making a visual selection,
  2. pressing the mapping above,

which is not too shabby.

Note that there is still room for improvement:

  • the initial pattern, \s, won't handle uneven spacing well,
  • we could make a custom operator instead of/as well as a visual mode mapping.

The first "issue" can be dealt with by sprinkling a little bit of vimscript over our substitutions:

function! ExpandList()
    silent s/\%V.*\%V/\="\r" . submatch(0)
            \ ->split(',')
            \ ->map({ i, v -> v->trim() })
            \ ->join(",\r") . "\r"/g
    silent normal ='[
endfunction
  • instead of the two-steps hack from earlier, we use a single substitution on the whole visual selection,
  • the replace part of the substitution is an expression where…
    • submatch(0) represents the whole match,
    • split(',') splits the string on commas (which became relevant again!) into a list,
    • map({ i, v -> v->trim() }) removes any whitespace padding from each entry in the list, this is done to address possible unevenness in the original text (think ["foo","bar" , "baz" ]),
    • join(",\r") puts it all back into a single string, separated with commas and \r,
    • additional \rs are added at the beginning and the end of the replacement string to ensure that the brackets are on their own lines.

See :help sub-replace-expression, :help split(), :help map(), :help trim(), and :help join().

Since we are at it, why don't we deal with the second "issue"?

The "operator+motion" model is one of those life-changing Vim features. Instead of

select some text and then do something with it

which is where we currently are, we

do something with some text

which manages to be both succinct and expressive at the same time.

And Vim indeed lets us create our own operators: :help map-operator.

As per the documentation above, we only need to do some minor modifications to our function definition:

function! ExpandList(type, ...)
    silent s/\%V.*\%V/\="\r" . submatch(0)
            \ ->split(',')
            \ ->map({ i, v -> v->trim()})
            \ ->join(",\r") . "\r"/g
    silent normal ='[
endfunction

and to our mapping:

nnoremap <key> :set opfunc=ExpandList<CR>g@

And we have a pretty clean and idiomatic solution: our own "Expand list" operator:

custom operator

Upvotes: 5

Related Questions