summaryrefslogtreecommitdiffstats
path: root/scripts/closetag.vim
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/closetag.vim')
-rw-r--r--scripts/closetag.vim327
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