]> Tony Duckles's Git Repositories (git.nynim.org) - dotfiles.git/blob - .vim/scripts/closetag.vim
.vim: Initial .vimrc and .vim/*
[dotfiles.git] / .vim / scripts / closetag.vim
1 " File: closetag.vim
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
6 " Version: 0.9.1
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
28 "
29 " Description:
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
37 " first column.
38 "
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
42 " param.
43 "
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.
46 "
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.
56 "
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
62 " problem.
63 "
64 " Install:
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.
70 "
71 " Example:
72 " :let b:closetag_html_style=1
73 " :source ~/.vim/scripts/closetag.vim
74 "
75 " For greater convenience, load this script in an autocommand:
76 " :au Filetype html,xml,xsl source ~/.vim/scripts/closetag.vim
77 "
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.
81 "
82 " Configuration Variables:
83 "
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
88 " ignorecase option.
89 "
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.
93 "
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.
97 "
98 " Changelog:
99 " May 24, 2005 Tuesday
100 " * Changed function names to be script-local to avoid conflicts with other
101 " scripts' stack implementations.
102 "
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...
107 "
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
114 "
115 " * Changed the closetag_html_style variable to be buffer-local rather than
116 " global.
117 "
118 " * Expanded documentation
119
120 "------------------------------------------------------------------------------
121 " User configurable settings
122 "------------------------------------------------------------------------------
123
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=""
131 endif
132 endif
133
134 " Has this already been loaded?
135 if exists("loaded_closetag")
136 finish
137 endif
138 let loaded_closetag=1
139
140 " set up mappings for tag closing
141 inoremap <C-_> <C-R>=GetCloseTag()<CR>
142 map <C-_> a<C-_><ESC>
143
144 "------------------------------------------------------------------------------
145 " Tag closer - uses the stringstack implementation below
146 "------------------------------------------------------------------------------
147
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()
162
163 let tagpat='</\=\(\k\|[-:]\)\+\|/>'
164 " Search for: closing tags </tag, opening tags <tag, and unary tag ends />
165 while (linenum>0)
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)
172 if first
173 let line=strpart(line,0,lineend)
174 else
175 let lineend=strlen(line)
176 endif
177 let b:lineTagStack=""
178 let mpos=0
179 let b:TagCol=0
180 " Search the current line in the forward direction, pushing any tags
181 " onto a special stack for the current line
182 while (mpos > -1)
183 let mpos=matchend(line,tagpat)
184 if mpos > -1
185 let b:TagCol=b:TagCol+mpos
186 let tag=matchstr(line,tagpat)
187
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")
191 endif
192 "echo "Tag: ".tag." ending at position ".mpos." in '".line."'."
193 let lineend=lineend-mpos
194 let line=strpart(line,mpos,lineend)
195 endif
196 endwhile
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
204 return tag
205 else
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
211 echohl Error
212 echon "\rError:"
213 echohl None
214 echo " tag mismatch: <".tag."> doesn't match <".endtag.">. (Line ".linenum." Tagstack: ".b:TagStack.")"
215 return ""
216 endif
217 endif
218 endwhile
219 let linenum=linenum-1 | let first=0
220 endwhile
221 " At this point, we have exhausted the file and not found any opening tag
222 echo "No opening tags."
223 return ""
224 endfunction
225
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")
230 if tag == ""
231 return ""
232 else
233 return "</".tag.">"
234 endif
235 endfunction
236
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'
241 endfunction
242
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'
246 endfunction
247
248 "------------------------------------------------------------------------------
249 " String Stacks
250 "------------------------------------------------------------------------------
251 " These are strings of whitespace-separated elements, matched using the \< and
252 " \> patterns after setting the iskeyword option.
253 "
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.)
257
258 " Helper functions
259 function! s:SetKeywords()
260 let g:IsKeywordBak=&iskeyword
261 let &iskeyword="33-255"
262 endfunction
263
264 function! s:RestoreKeywords()
265 let &iskeyword=g:IsKeywordBak
266 endfunction
267
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
272 else
273 exe "let ".a:sname."=a:el"
274 endif
275 endfunction
276
277 " Check whether the stack is empty
278 function! s:EmptystackP(sname)
279 exe "let stack=".a:sname
280 if match(stack,"^ *$") == 0
281 return 1
282 else
283 return 0
284 endif
285 endfunction
286
287 " Return 1 if el is in stack sname, else 0.
288 function! s:Instack(el, sname)
289 exe "let stack=".a:sname
290 call s:SetKeywords()
291 let m=match(stack, "\\<".a:el."\\>")
292 call s:RestoreKeywords()
293 if m < 0
294 return 0
295 else
296 return 1
297 endif
298 endfunction
299
300 " Return the first element in the stack
301 function! s:Peekstack(sname)
302 call s:SetKeywords()
303 exe "let stack=".a:sname
304 let top=matchstr(stack, "\\<.\\{-1,}\\>")
305 call s:RestoreKeywords()
306 return top
307 endfunction
308
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."
313 return ""
314 endif
315 exe "let stack=".a:sname
316 " Find the first space, loc is 0-based. Marks the end of 1st elt in stack.
317 call s:SetKeywords()
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()
322 return top
323 endfunction
324
325 function! s:Clearstack(sname)
326 exe "let ".a:sname."=''"
327 endfunction