2 " Summary: Functions and mappings to close open HTML/XML tags
3 " Uses: <C-_> -- close matching open tag
4 " Author: Steven Mueller <diffusor@ugcs.caltech.edu>
5 " Last Modified: Tue May 24 13:29:48 PDT 2005
7 " XXX - breaks if close attempted while XIM is in preedit mode
8 " TODO - allow usability as a global plugin -
9 " Add g:unaryTagsStack - always contains html tags settings
10 " and g:closetag_default_xml - user should define this to default to xml
11 " When a close is attempted but b:unaryTagsStack undefined,
12 " use b:closetag_html_style to determine if the file is to be treated
13 " as html or xml. Failing that, check the filetype for xml or html.
14 " Finally, default to g:closetag_html_style.
15 " If the file is html, let b:unaryTagsStack=g:unaryTagsStack
16 " otherwise, let b:unaryTagsStack=""
17 " TODO - make matching work for all comments
18 " -- kinda works now, but needs syn sync minlines to be very long
19 " -- Only check whether in syntax in the beginning, then store comment tags
20 " in the tagstacks to determine whether to move into or out of comment mode
21 " TODO - The new normal mode mapping clears recent messages with its <ESC>, and
22 " it doesn't fix the null-undo issue for vim 5.7 anyway.
23 " TODO - make use of the following neat features:
24 " -- the ternary ?: operator
25 " -- :echomsg and :echoerr
26 " -- curly brace expansion for variables and function name definitions?
27 " -- check up on map <blah> \FuncName
30 " This script eases redundant typing when writing html or xml files (even if
31 " you're very good with ctrl-p and ctrl-n :). Hitting ctrl-_ will initiate a
32 " search for the most recent open tag above that is not closed in the
33 " intervening space and then insert the matching close tag at the cursor. In
34 " normal mode, the close tag is inserted one character after cursor rather than
35 " at it, as if a<C-_> had been used. This allows putting close tags at the
36 " ends of lines while in normal mode, but disallows inserting them in the
39 " For HTML, a configurable list of tags are ignored in the matching process.
40 " By default, the following tags will not be matched and thus not closed
41 " automatically: area, base, br, dd, dt, hr, img, input, link, meta, and
44 " For XML, all tags must have a closing match or be terminated by />, as in
45 " <empty-element/>. These empty element tags are ignored for matching.
47 " Comment checking is now handled by vim's internal syntax checking. If tag
48 " closing is initiated outside a comment, only tags outside of comments will
49 " be matched. When closing tags in comments, only tags within comments will
50 " be matched, skipping any non-commented out code (wee!). However, the
51 " process of determining the syntax ID of an arbitrary position can still be
52 " erroneous if a comment is not detected because the syntax highlighting is
53 " out of sync, or really slow if syn sync minlines is large.
54 " Set the b:closetag_disable_synID variable to disable this feature if you
55 " have really big chunks of comment in your code and closing tags is too slow.
57 " If syntax highlighting is not enabled, comments will not be handled very
58 " well. Commenting out HTML in certain ways may cause a "tag mismatch"
59 " message and no completion. For example, '<!--a href="blah">link!</a-->'
60 " between the cursor and the most recent unclosed open tag above causes
61 " trouble. Properly matched well formed tags in comments don't cause a
65 " To use, place this file in your standard vim scripts directory, and source
66 " it while editing the file you wish to close tags in. If the filetype is not
67 " set or the file is some sort of template with embedded HTML, you may force
68 " HTML style tag matching by first defining the b:closetag_html_style buffer
69 " variable. Otherwise, the default is XML style tag matching.
72 " :let b:closetag_html_style=1
73 " :source ~/.vim/scripts/closetag.vim
75 " For greater convenience, load this script in an autocommand:
76 " :au Filetype html,xml,xsl source ~/.vim/scripts/closetag.vim
78 " Also, set noignorecase for html files or edit b:unaryTagsStack to match your
79 " capitalization style. You may set this variable before or after loading the
80 " script, or simply change the file itself.
82 " Configuration Variables:
84 " b:unaryTagsStack Buffer local string containing a whitespace
85 " seperated list of element names that should be
86 " ignored while finding matching closetags. Checking
87 " is done according to the current setting of the
90 " b:closetag_html_style Define this (as with let b:closetag_html_style=1)
91 " and source the script again to set the
92 " unaryTagsStack to its default value for html.
94 " b:closetag_disable_synID Define this to disable comment checking if tag
95 " closing is too slow. This can be set or unset
96 " without having to source again.
99 " May 24, 2005 Tuesday
100 " * Changed function names to be script-local to avoid conflicts with other
101 " scripts' stack implementations.
103 " June 07, 2001 Thursday
104 " * Added comment handling. Currently relies on synID, so if syn sync
105 " minlines is small, the chance for failure is high, but if minlines is
106 " large, tagclosing becomes rather slow...
108 " * Changed normal mode closetag mapping to use <C-R> in insert mode
109 " rather than p in normal mode. This has 2 implications:
110 " - Tag closing no longer clobbers the unnamed register
111 " - When tag closing fails or finds no match, no longer adds to the undo
112 " buffer for recent vim 6.0 development versions.
113 " - However, clears the last message when closing tags in normal mode
115 " * Changed the closetag_html_style variable to be buffer-local rather than
118 " * Expanded documentation
120 "------------------------------------------------------------------------------
121 " User configurable settings
122 "------------------------------------------------------------------------------
124 " if html, don't close certain tags. Works best if ignorecase is set.
125 " otherwise, capitalize these elements according to your html editing style
126 if !exists("b:unaryTagsStack") || exists("b:closetag_html_style")
127 if &filetype == "html" || exists("b:closetag_html_style")
128 let b:unaryTagsStack="area base br dd dt hr img input link meta param"
129 else " for xsl and xsl
130 let b:unaryTagsStack=""
134 " Has this already been loaded?
135 if exists("loaded_closetag")
138 let loaded_closetag=1
140 " set up mappings for tag closing
141 inoremap <C-_> <C-R>=GetCloseTag()<CR>
142 map <C-_> a<C-_><ESC>
144 "------------------------------------------------------------------------------
145 " Tag closer - uses the stringstack implementation below
146 "------------------------------------------------------------------------------
148 " Returns the most recent unclosed tag-name
149 " (ignores tags in the variable referenced by a:unaryTagsStack)
150 function! GetLastOpenTag(unaryTagsStack)
151 " Search backwards through the file line by line using getline()
152 " Overall strategy (moving backwards through the file from the cursor):
153 " Push closing tags onto a stack.
154 " On an opening tag, if the tag matches the stack top, discard both.
155 " -- if the tag doesn't match, signal an error.
156 " -- if the stack is empty, use this tag
157 let linenum=line(".")
158 let lineend=col(".") - 1 " start: cursor position
159 let first=1 " flag for first line searched
160 let b:TagStack="" " main stack of tags
161 let startInComment=s:InComment()
163 let tagpat='</\=\(\k\|[-:]\)\+\|/>'
164 " Search for: closing tags </tag, opening tags <tag, and unary tag ends />
166 " Every time we see an end-tag, we push it on the stack. When we see an
167 " open tag, if the stack isn't empty, we pop it and see if they match.
168 " If no, signal an error.
169 " If yes, continue searching backwards.
170 " If stack is empty, return this open tag as the one that needs closing.
171 let line=getline(linenum)
173 let line=strpart(line,0,lineend)
175 let lineend=strlen(line)
177 let b:lineTagStack=""
180 " Search the current line in the forward direction, pushing any tags
181 " onto a special stack for the current line
183 let mpos=matchend(line,tagpat)
185 let b:TagCol=b:TagCol+mpos
186 let tag=matchstr(line,tagpat)
188 if exists("b:closetag_disable_synID") || startInComment==s:InCommentAt(linenum, b:TagCol)
189 let b:TagLine=linenum
190 call s:Push(matchstr(tag,'[^<>]\+'),"b:lineTagStack")
192 "echo "Tag: ".tag." ending at position ".mpos." in '".line."'."
193 let lineend=lineend-mpos
194 let line=strpart(line,mpos,lineend)
197 " Process the current line stack
198 while (!s:EmptystackP("b:lineTagStack"))
199 let tag=s:Pop("b:lineTagStack")
200 if match(tag, "^/") == 0 "found end tag
201 call s:Push(tag,"b:TagStack")
202 "echo linenum." ".b:TagStack
203 elseif s:EmptystackP("b:TagStack") && !s:Instack(tag, a:unaryTagsStack) "found unclosed tag
206 let endtag=s:Peekstack("b:TagStack")
207 if endtag == "/".tag || endtag == "/"
208 call s:Pop("b:TagStack") "found a open/close tag pair
209 "echo linenum." ".b:TagStack
210 elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
214 echo " tag mismatch: <".tag."> doesn't match <".endtag.">. (Line ".linenum." Tagstack: ".b:TagStack.")"
219 let linenum=linenum-1 | let first=0
221 " At this point, we have exhausted the file and not found any opening tag
222 echo "No opening tags."
226 " Returns closing tag for most recent unclosed tag, respecting the
227 " current setting of b:unaryTagsStack for tags that should not be closed
228 function! GetCloseTag()
229 let tag=GetLastOpenTag("b:unaryTagsStack")
237 " return 1 if the cursor is in a syntactically identified comment field
238 " (fails for empty lines: always returns not-in-comment)
239 function! s:InComment()
240 return synIDattr(synID(line("."), col("."), 0), "name") =~ 'Comment'
243 " return 1 if the position specified is in a syntactically identified comment field
244 function! s:InCommentAt(line, col)
245 return synIDattr(synID(a:line, a:col, 0), "name") =~ 'Comment'
248 "------------------------------------------------------------------------------
250 "------------------------------------------------------------------------------
251 " These are strings of whitespace-separated elements, matched using the \< and
252 " \> patterns after setting the iskeyword option.
254 " The sname argument should contain a symbolic reference to the stack variable
255 " on which method should operate on (i.e., sname should be a string containing
256 " a fully qualified (ie: g:, b:, etc) variable name.)
259 function! s:SetKeywords()
260 let g:IsKeywordBak=&iskeyword
261 let &iskeyword="33-255"
264 function! s:RestoreKeywords()
265 let &iskeyword=g:IsKeywordBak
268 " Push el onto the stack referenced by sname
269 function! s:Push(el, sname)
270 if !s:EmptystackP(a:sname)
271 exe "let ".a:sname."=a:el.' '.".a:sname
273 exe "let ".a:sname."=a:el"
277 " Check whether the stack is empty
278 function! s:EmptystackP(sname)
279 exe "let stack=".a:sname
280 if match(stack,"^ *$") == 0
287 " Return 1 if el is in stack sname, else 0.
288 function! s:Instack(el, sname)
289 exe "let stack=".a:sname
291 let m=match(stack, "\\<".a:el."\\>")
292 call s:RestoreKeywords()
300 " Return the first element in the stack
301 function! s:Peekstack(sname)
303 exe "let stack=".a:sname
304 let top=matchstr(stack, "\\<.\\{-1,}\\>")
305 call s:RestoreKeywords()
309 " Remove and return the first element in the stack
310 function! s:Pop(sname)
311 if s:EmptystackP(a:sname)
312 echo "Error! Stack ".a:sname." is empty and can't be popped."
315 exe "let stack=".a:sname
316 " Find the first space, loc is 0-based. Marks the end of 1st elt in stack.
318 let loc=matchend(stack,"\\<.\\{-1,}\\>")
319 exe "let ".a:sname."=strpart(stack, loc+1, strlen(stack))"
320 let top=strpart(stack, match(stack, "\\<"), loc)
321 call s:RestoreKeywords()
325 function! s:Clearstack(sname)
326 exe "let ".a:sname."=''"