6

How can I quickly shift only inner contents of HTML tag X, when they are on separate lines to the opening and closing X tag? Example below: I want to shift only 2 lines with <p> tags when the cursor is somewhere on <div> or </div>.

<div>
  <p>This is some paragraph.</p>
  <p>And this is other paragraph.</p>
</div>

When the cursor is somewhere on <div> I try to do >it, but the problem is it is written in a way which select also newline character after <div> and whitespace before </div>, and so the start and ending tags are also shifted (ending tag might be left unshifted if it's on the very beginning of line), which is unwanted by me.

What I usually do is select everything between <div>s by vit then go one line up on the end of the selection by k, switch to the other end by o and go one line down on the start of the selection by j, only then shift by >. The whole sequence is vitkoj>. Could you do it somehow faster (and more naturally)?

I'm mostly interested in answers using pure Vim functionality without plugins, but HTML plugins recommendations are also welcome.

  • Just to be clear, you're only trying to indent the two <p> tag lines, correct? At first I was thinking this must be super easy, but when I went to try it I realized I have no idea how to do it. – Pak Jul 26 '17 at 17:47
  • @Pak Yes, I'm trying to indent 2 <p> tag lines :). – Robert Kusznier Jul 27 '17 at 8:28
  • 1
    Weird. I would have expected >it to do just that, but apparently it also indents the start <div> tag. – Cristian Lupascu Jul 27 '17 at 12:00
2

That's a great opportunity to leverage text objects (see :help text-objects). The following code extends the normal behavior of the it text object (see :help it and :help tag-blocks) to make it consider multi-line HTML tags content as described in your post. In that way, you could also type in vit>, >it, dit, cit, yit and so on, with the extended behavior in place. You can put this code at the end of the vimrc file:

" 0: Strict, consider non-blank characters before and after the tags.
" 1: Flexible, ignore non-blank characters before and after the tags.
let g:innerMultilineHTMLTagMode = 0

function! InnerMultilineHTMLTag()
   " Get the position of the first line of the last selected Visual area.
   let openingMark =  getpos("'<")

   " Get the position of the last line of the last selected Visual area.
   let closingMark = getpos("'>")

   " Check whether both marks are on the same line.
   if openingMark[1] != closingMark[1] 

      " Get the lines where the marks are on.
      let openingLine = getline(openingMark[1])
      let closingLine = getline(closingMark[1])

      " Check whether there's nothing appended to the opening tag.
      if g:innerMultilineHTMLTagMode == 1  ||
         \ match( openingLine, '\S',  openingMark[2] - 1) == -1 

         " Check whether the closing tag is at the beginning of the line.
         if match( closingLine, "$" ) + 1  ==  closingMark[2]
            " Restore and adjust the last Visual area.
            normal! gvVojo
            return

         " Check whether there's nothing prepended to the closing tag.
         elseif g:innerMultilineHTMLTagMode == 1  || 
            \   match( closingLine, '\S\%<' . closingMark[2] . "c" ) == -1
            " Restore and adjust the last Visual area.
            normal! gvVkojo
            return
         endif
      endif 
   endif

   " Do nothing. Restore the last Visual area.
   normal! gv
endfunction

function! ToggleInnerMultilineHTMLTagMode()
   if g:innerMultilineHTMLTagMode == 0 
      let g:innerMultilineHTMLTagMode = 1 
      echo "it text object is now flexible"
   else
      let g:innerMultilineHTMLTagMode = 0 
      echo "it text object is now strict"
   endif
endfunction

" Map to set the multi-line HTML tag mode.
nnoremap - :call ToggleInnerMultilineHTMLTagMode()<CR>

" Map to extend the behavior of the 'it' text object to create linewise
" visual areas within multi-line HTML tags.
" See https://vi.stackexchange.com/q/13050/6698
vnoremap it it:<C-U>call InnerMultilineHTMLTag()<CR>
omap it :normal vit<CR>

There are two modes that can be toggled by hitting -:

  • Strict mode: (default) Consider the text before and after the targeted tags. If there are non-blank characters after the opening tag or before the closing tag, the it text object will fall back on the normal behavior. Otherwise It'll make the visual area linewise and only select the content within the tags, ignoring blanks both after the opening tag and before the closing tag.

  • Flexible mode: Ignore the text before and after the targeted tags.

The flexible mode is useful when the code has the following form:

<div class="one" > <div class="two" >
  <p>This is some paragraph.</p>
  <p>And this is other paragraph.</p>
</div> </div>

and some action is targeted to the indented lines regardless of whether the cursor is on either <div class="one" > or <div class="two" > tag.

Bonus

It's also useful to create a text object to allow indented text blocks to be treated as text objects. For example, if the cursor is on the word some in the first <p> tag of the example given by Robert, you could use a custom ai text object, which stands for “an indent”, to select the line where the cursor is on as well as the next one (both with the same indentation level) and re-indent them with >ai. Read Indent text object at Vim Tips Wiki for more information.

Reference

  • Thanks! That is the best answer I think. The only issue is that setting g:innerMultilineHTMLTagMode variable doesn't change the behaviour of it - in both modes it behaves the same, ignoring the whitespace after opening tag and before the closing tag. Could you correct that? – Robert Kusznier Jul 28 '17 at 8:36
  • Sure! But ignoring the whitespace after opening tag and before the closing tag is the intended behavior in both modes. The difference is that when flexible mode is on, it'll work even if there are some tags stacked on the same line as the last example above . Maybe I misunderstood your question. Could you please add an example with the issue and the expected result? – Jair López Jul 28 '17 at 10:13
  • Then I simply misunderstood what strict and flexible modes should do :). – Robert Kusznier Jul 31 '17 at 8:38
  • I've been using your solution for some time and while it works fine, there is one area where it needs improvement - when I want to increase/reduce indentation multiple times, it'd expect to execute >it/<it once and then repeat it with .. The problem is it doesn't work. After pressing >it/<it, the cursor moves on the inner tag (the end of the selection), while for . to work, it'd need to stay on the outer tag. I hope you understand what I mean, cause it's not that easy to explain in a short comment. Do you have some idea how to make >it/<it repeatable by .? – Robert Kusznier May 28 '18 at 8:27
3

Could you do it somehow faster (and more naturally)?

In Vim you can do everything faster and more natural by adding mappings, macros, and abbreviations. Here is a mapping that you could use to select lines inside a tag from the first inner tag to the last. And if there are no inner tags, that will select the lines of a tag itself:

:nnoremap <leader>it vito/<<CR>o?><CR>V

    Explanation:
    vit                  -- Visual mode, select inner block of a tag
       o/<<CR>           -- Limit selection till before the first inner tag
              o?><CR>    -- Limit selection till after the last inner tag
                     V   -- Make selection linewise

mapping_demo

You need the o?><CR> because if there are characters on the line before the closing tag, the selection will grab that line as well. Replace V with > if you just want to indent it and don't need the selection by itself. Hope that helps.

Also, assuming that everything from opening to matching enclosing tag is properly indented, you could do =it or =at. However if indentation is messed up, that will fix it instead:

equals_it_demo

  • I understand your answer and it's exactly what I need. Still for some reason running :nnoremap <leader>it vito/<<CR>o?><CR>V (nor adding that to my vimrc) doesn't change the behaviour of it (it still selects all the whitespace after opening tag and before closing tag). When I run sequence vito/<<CR>o?><CR>V everything works fine, but adding the mapping does not. What could be the reason? BTW that is a beautifully written answer. Thanks! – Robert Kusznier Jul 27 '17 at 14:39
  • B Okay, I know what's the problem - it's that I have <leader> set to \ in my .vimrc. What does <leader> does in this mapping? Could I remove it? – Robert Kusznier Jul 27 '17 at 14:43
  • @Robert The it stays unchanged. To run this you need to press <leader>it while in Normal Mode. If you didn't change your leader key, it is probably a backslash, so you need to type \it. You can bind it to whatever free combination you want, for example \abc or \x. I chose \it just for convenience. – user13463 Jul 27 '17 at 14:50
  • @Robert in the demo, status line under the percentage on the right, it is showing the commands I type. The \it means tap backslash, tap i, tap t. You can remap the original it, but then you will not be able to use the original it anymore. If you remove the change of leader key from your .vimrc it will default to backslash. See :help leader. – user13463 Jul 27 '17 at 14:59
  • I understand. Thanks for explanation. I accepted your answer, as I think it answers my question, making the process much more pain-free and explaining everything very well. – Robert Kusznier Jul 27 '17 at 17:12

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.