0

I am writing a mapping to autocomplete tags

Pattern: <.*?>, matches complete HTML tags only.

The goal is to lookbehind and see if there exists a text like <html>.

Mapping: inoremap <expr> <leader>< TagComplete()


func! TagComplete() {
  let match=search('<.*?>', 'b', '.')
  return match
}

Once I run the Mapping in insert mode after typing <html>, on execution of function TagComplete, if the pattern is matched, the match variable should have value <html> and at the cursor position <\html> should be inserted. Once done the editor should be in insert mode, keeping the cursor between the tags.

I am having difficulty in returning the match applying search().

Also vim does not understand non-greedy ? in pattern.


EDIT:

I am able to print closing tag:

inoremap <expr> <C-V> AutoCompleteTag()

func! AutoCompleteTag()
  let l:match = search('<[^/].\{-}>', 'bn', line("."))
  let l:pat="<\zs\/.\{-}\ze>"
  if l:match > 0 
    let l:lastMatch=MatchStrLast(getline(l:match), '<\zs.\{-}\ze>')
    if l:lastMatch!~"\/.*"
      return '</'.l:lastMatch.'>'                                                                                                                                                                           
    endif
  endif
endfunc

" use the {count} parameter for matchstr() to increment your way through the
" string 
function! MatchStrLast(expr, pat, ...)
  let start = a:0 ? a:1 : 0 
  let last = ''
  let cnt = 1 
  let found = match(a:expr, a:pat, start, cnt)
  while found != -1
    let last=matchstr(a:expr, a:pat, start, cnt)
    let cnt += 1
    let found = match(a:expr, a:pat, start, cnt)
  endwhile
  return last 
endfunction

Explanation:

  • search('<[^/].\{-}>', 'bn', line(".")), I am searching to see if a valid tag pattern exits on current line. If yes, store line number in l:match. Is there a way to search backwards from cursor position and match the opening tag.
  • MatchStrLast(getline(l:match), '<\zs.\{-}\ze>'), gets the name of the tag.

Concerns:

I am not able to find a way to move the cursor so that it is in the middle of the tags.

Once the mapping is run, it prints a 0 if the match is not found. How not to print anything in case the regexp does not match the pattern?

Also, in if condition if l:lastMatch!~"\/.*", I tried the pattern <\zs\/.\{-}\ze> in place of \/.*, that I expected to match string "/S.Component", but it did not. Reason?.

Since, I am new to vimscript, I appreciate anyone pointing out an anti-pattern and please suggest improvements.

4
  • 2
    .*? is not a valid vim regex pattern, have a look at :h non-greedy for the correct way of non-greedy patterns. Commented Feb 24, 2020 at 16:17
  • what should be come after <.*>\{-} for non greedy match? With this pattern it just matched entire tags with content included Commented Feb 24, 2020 at 17:51
  • How about a different approach? <[^>]*> to match complete tags? Commented Feb 24, 2020 at 19:14
  • Yes, this helps. After this in order to match contents inside the tag, I tried \/\?\zs\a\+\ze[ >]. But I can't get it to match a tag name "S.Container" in <S.Container>, but it does match "container" in <container> Commented Feb 24, 2020 at 19:19

1 Answer 1

2

I feel like this is all overly complicated, you can realize this with just a few lines of regex:

s/<\(\w\+\)>\%#/&<\/\1>/g

This expression will allow you to add a closing tag after the cursor:
- < matches the start of the opening tag
- \w\+ matches the tag content, we include it in braces so that we can use it in the replacement
- > matches the end of the existing tag
- \%# matches the cursor (this is specific to vim :h ordinary-atom)

And for the replacement:
- & will repeat the whole match (so the opening tag)
- <\/ starts the closing tag
- \1 repeats what was in the braces (the tag name)
- > ends the closing tag

Once we're there, the tag is successfully closed, but the cursor moves! We can prevent that by setting marks:

func! AutoCompleteTag()
    let saveMMark = getpos("'m")
    let saveSearch = @/

    call setpos("'m", getpos("."))
    sil! s/<\(\w\+\)>\%#/&<\/\1>/g
    call setpos(".", getpos("'m"))

    call setpos("'m", saveMMark)
    let @/ = saveSearch
endfunction

(I also save and restore the search pattern and the m cursor position there)

One last small problem: you can repeat this operation an therefore close the tag multiple times... This can be prevented by... More regex!

s/<\(\w\+\)>\%#\ze\(.*<\/\1>\)\@!/&<\/\1>/g

Here, we use \(...\)\@! to say that we do not want this That only works if the closing tag is on the same line though, if you want to extend it to work on multiple lines, you might be interested in searchpairpos()

for the \@!, look into :h pattern-overview

TL;DR:
Here's the last version of the function:

func! AutoCompleteTag()
    let saveMMark = getpos("'m")
    let saveSearch = @/

    call setpos("'m", getpos("."))
    sil! s/<\(\w\+\)>\%#\ze\(.*<\/\1>\)\@!/&<\/\1>/g
    call setpos(".", getpos("'m"))

    call setpos("'m", saveMMark)
    let @/ = saveSearch
endfunction

inoremap <C-v> <CMD>call AutoCompleteTag()<CR>
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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