diff options
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/closetag.vim | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/scripts/closetag.vim b/scripts/closetag.vim new file mode 100644 index 0000000..6ce41fb --- /dev/null +++ b/scripts/closetag.vim @@ -0,0 +1,327 @@ +" File: closetag.vim +" Summary: Functions and mappings to close open HTML/XML tags +" Uses: <C-_> -- close matching open tag +" Author: Steven Mueller <diffusor@ugcs.caltech.edu> +" Last Modified: Tue May 24 13:29:48 PDT 2005 +" Version: 0.9.1 +" XXX - breaks if close attempted while XIM is in preedit mode +" TODO - allow usability as a global plugin - +" Add g:unaryTagsStack - always contains html tags settings +" and g:closetag_default_xml - user should define this to default to xml +" When a close is attempted but b:unaryTagsStack undefined, +" use b:closetag_html_style to determine if the file is to be treated +" as html or xml. Failing that, check the filetype for xml or html. +" Finally, default to g:closetag_html_style. +" If the file is html, let b:unaryTagsStack=g:unaryTagsStack +" otherwise, let b:unaryTagsStack="" +" TODO - make matching work for all comments +" -- kinda works now, but needs syn sync minlines to be very long +" -- Only check whether in syntax in the beginning, then store comment tags +" in the tagstacks to determine whether to move into or out of comment mode +" TODO - The new normal mode mapping clears recent messages with its <ESC>, and +" it doesn't fix the null-undo issue for vim 5.7 anyway. +" TODO - make use of the following neat features: +" -- the ternary ?: operator +" -- :echomsg and :echoerr +" -- curly brace expansion for variables and function name definitions? +" -- check up on map <blah> \FuncName +" +" Description: +" This script eases redundant typing when writing html or xml files (even if +" you're very good with ctrl-p and ctrl-n :). Hitting ctrl-_ will initiate a +" search for the most recent open tag above that is not closed in the +" intervening space and then insert the matching close tag at the cursor. In +" normal mode, the close tag is inserted one character after cursor rather than +" at it, as if a<C-_> had been used. This allows putting close tags at the +" ends of lines while in normal mode, but disallows inserting them in the +" first column. +" +" For HTML, a configurable list of tags are ignored in the matching process. +" By default, the following tags will not be matched and thus not closed +" automatically: area, base, br, dd, dt, hr, img, input, link, meta, and +" param. +" +" For XML, all tags must have a closing match or be terminated by />, as in +" <empty-element/>. These empty element tags are ignored for matching. +" +" Comment checking is now handled by vim's internal syntax checking. If tag +" closing is initiated outside a comment, only tags outside of comments will +" be matched. When closing tags in comments, only tags within comments will +" be matched, skipping any non-commented out code (wee!). However, the +" process of determining the syntax ID of an arbitrary position can still be +" erroneous if a comment is not detected because the syntax highlighting is +" out of sync, or really slow if syn sync minlines is large. +" Set the b:closetag_disable_synID variable to disable this feature if you +" have really big chunks of comment in your code and closing tags is too slow. +" +" If syntax highlighting is not enabled, comments will not be handled very +" well. Commenting out HTML in certain ways may cause a "tag mismatch" +" message and no completion. For example, '<!--a href="blah">link!</a-->' +" between the cursor and the most recent unclosed open tag above causes +" trouble. Properly matched well formed tags in comments don't cause a +" problem. +" +" Install: +" To use, place this file in your standard vim scripts directory, and source +" it while editing the file you wish to close tags in. If the filetype is not +" set or the file is some sort of template with embedded HTML, you may force +" HTML style tag matching by first defining the b:closetag_html_style buffer +" variable. Otherwise, the default is XML style tag matching. +" +" Example: +" :let b:closetag_html_style=1 +" :source ~/.vim/scripts/closetag.vim +" +" For greater convenience, load this script in an autocommand: +" :au Filetype html,xml,xsl source ~/.vim/scripts/closetag.vim +" +" Also, set noignorecase for html files or edit b:unaryTagsStack to match your +" capitalization style. You may set this variable before or after loading the +" script, or simply change the file itself. +" +" Configuration Variables: +" +" b:unaryTagsStack Buffer local string containing a whitespace +" seperated list of element names that should be +" ignored while finding matching closetags. Checking +" is done according to the current setting of the +" ignorecase option. +" +" b:closetag_html_style Define this (as with let b:closetag_html_style=1) +" and source the script again to set the +" unaryTagsStack to its default value for html. +" +" b:closetag_disable_synID Define this to disable comment checking if tag +" closing is too slow. This can be set or unset +" without having to source again. +" +" Changelog: +" May 24, 2005 Tuesday +" * Changed function names to be script-local to avoid conflicts with other +" scripts' stack implementations. +" +" June 07, 2001 Thursday +" * Added comment handling. Currently relies on synID, so if syn sync +" minlines is small, the chance for failure is high, but if minlines is +" large, tagclosing becomes rather slow... +" +" * Changed normal mode closetag mapping to use <C-R> in insert mode +" rather than p in normal mode. This has 2 implications: +" - Tag closing no longer clobbers the unnamed register +" - When tag closing fails or finds no match, no longer adds to the undo +" buffer for recent vim 6.0 development versions. +" - However, clears the last message when closing tags in normal mode +" +" * Changed the closetag_html_style variable to be buffer-local rather than +" global. +" +" * Expanded documentation + +"------------------------------------------------------------------------------ +" User configurable settings +"------------------------------------------------------------------------------ + +" if html, don't close certain tags. Works best if ignorecase is set. +" otherwise, capitalize these elements according to your html editing style +if !exists("b:unaryTagsStack") || exists("b:closetag_html_style") + if &filetype == "html" || exists("b:closetag_html_style") + let b:unaryTagsStack="area base br dd dt hr img input link meta param" + else " for xsl and xsl + let b:unaryTagsStack="" + endif +endif + +" Has this already been loaded? +if exists("loaded_closetag") + finish +endif +let loaded_closetag=1 + +" set up mappings for tag closing +inoremap <C-_> <C-R>=GetCloseTag()<CR> +map <C-_> a<C-_><ESC> + +"------------------------------------------------------------------------------ +" Tag closer - uses the stringstack implementation below +"------------------------------------------------------------------------------ + +" Returns the most recent unclosed tag-name +" (ignores tags in the variable referenced by a:unaryTagsStack) +function! GetLastOpenTag(unaryTagsStack) + " Search backwards through the file line by line using getline() + " Overall strategy (moving backwards through the file from the cursor): + " Push closing tags onto a stack. + " On an opening tag, if the tag matches the stack top, discard both. + " -- if the tag doesn't match, signal an error. + " -- if the stack is empty, use this tag + let linenum=line(".") + let lineend=col(".") - 1 " start: cursor position + let first=1 " flag for first line searched + let b:TagStack="" " main stack of tags + let startInComment=s:InComment() + + let tagpat='</\=\(\k\|[-:]\)\+\|/>' + " Search for: closing tags </tag, opening tags <tag, and unary tag ends /> + while (linenum>0) + " Every time we see an end-tag, we push it on the stack. When we see an + " open tag, if the stack isn't empty, we pop it and see if they match. + " If no, signal an error. + " If yes, continue searching backwards. + " If stack is empty, return this open tag as the one that needs closing. + let line=getline(linenum) + if first + let line=strpart(line,0,lineend) + else + let lineend=strlen(line) + endif + let b:lineTagStack="" + let mpos=0 + let b:TagCol=0 + " Search the current line in the forward direction, pushing any tags + " onto a special stack for the current line + while (mpos > -1) + let mpos=matchend(line,tagpat) + if mpos > -1 + let b:TagCol=b:TagCol+mpos + let tag=matchstr(line,tagpat) + + if exists("b:closetag_disable_synID") || startInComment==s:InCommentAt(linenum, b:TagCol) + let b:TagLine=linenum + call s:Push(matchstr(tag,'[^<>]\+'),"b:lineTagStack") + endif + "echo "Tag: ".tag." ending at position ".mpos." in '".line."'." + let lineend=lineend-mpos + let line=strpart(line,mpos,lineend) + endif + endwhile + " Process the current line stack + while (!s:EmptystackP("b:lineTagStack")) + let tag=s:Pop("b:lineTagStack") + if match(tag, "^/") == 0 "found end tag + call s:Push(tag,"b:TagStack") + "echo linenum." ".b:TagStack + elseif s:EmptystackP("b:TagStack") && !s:Instack(tag, a:unaryTagsStack) "found unclosed tag + return tag + else + let endtag=s:Peekstack("b:TagStack") + if endtag == "/".tag || endtag == "/" + call s:Pop("b:TagStack") "found a open/close tag pair + "echo linenum." ".b:TagStack + elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error + echohl Error + echon "\rError:" + echohl None + echo " tag mismatch: <".tag."> doesn't match <".endtag.">. (Line ".linenum." Tagstack: ".b:TagStack.")" + return "" + endif + endif + endwhile + let linenum=linenum-1 | let first=0 + endwhile + " At this point, we have exhausted the file and not found any opening tag + echo "No opening tags." + return "" +endfunction + +" Returns closing tag for most recent unclosed tag, respecting the +" current setting of b:unaryTagsStack for tags that should not be closed +function! GetCloseTag() + let tag=GetLastOpenTag("b:unaryTagsStack") + if tag == "" + return "" + else + return "</".tag.">" + endif +endfunction + +" return 1 if the cursor is in a syntactically identified comment field +" (fails for empty lines: always returns not-in-comment) +function! s:InComment() + return synIDattr(synID(line("."), col("."), 0), "name") =~ 'Comment' +endfunction + +" return 1 if the position specified is in a syntactically identified comment field +function! s:InCommentAt(line, col) + return synIDattr(synID(a:line, a:col, 0), "name") =~ 'Comment' +endfunction + +"------------------------------------------------------------------------------ +" String Stacks +"------------------------------------------------------------------------------ +" These are strings of whitespace-separated elements, matched using the \< and +" \> patterns after setting the iskeyword option. +" +" The sname argument should contain a symbolic reference to the stack variable +" on which method should operate on (i.e., sname should be a string containing +" a fully qualified (ie: g:, b:, etc) variable name.) + +" Helper functions +function! s:SetKeywords() + let g:IsKeywordBak=&iskeyword + let &iskeyword="33-255" +endfunction + +function! s:RestoreKeywords() + let &iskeyword=g:IsKeywordBak +endfunction + +" Push el onto the stack referenced by sname +function! s:Push(el, sname) + if !s:EmptystackP(a:sname) + exe "let ".a:sname."=a:el.' '.".a:sname + else + exe "let ".a:sname."=a:el" + endif +endfunction + +" Check whether the stack is empty +function! s:EmptystackP(sname) + exe "let stack=".a:sname + if match(stack,"^ *$") == 0 + return 1 + else + return 0 + endif +endfunction + +" Return 1 if el is in stack sname, else 0. +function! s:Instack(el, sname) + exe "let stack=".a:sname + call s:SetKeywords() + let m=match(stack, "\\<".a:el."\\>") + call s:RestoreKeywords() + if m < 0 + return 0 + else + return 1 + endif +endfunction + +" Return the first element in the stack +function! s:Peekstack(sname) + call s:SetKeywords() + exe "let stack=".a:sname + let top=matchstr(stack, "\\<.\\{-1,}\\>") + call s:RestoreKeywords() + return top +endfunction + +" Remove and return the first element in the stack +function! s:Pop(sname) + if s:EmptystackP(a:sname) + echo "Error! Stack ".a:sname." is empty and can't be popped." + return "" + endif + exe "let stack=".a:sname + " Find the first space, loc is 0-based. Marks the end of 1st elt in stack. + call s:SetKeywords() + let loc=matchend(stack,"\\<.\\{-1,}\\>") + exe "let ".a:sname."=strpart(stack, loc+1, strlen(stack))" + let top=strpart(stack, match(stack, "\\<"), loc) + call s:RestoreKeywords() + return top +endfunction + +function! s:Clearstack(sname) + exe "let ".a:sname."=''" +endfunction |