1"  matchit.vim: (global plugin) Extended "%" matching
2"  Last Change: Fri Jan 25 10:00 AM 2008 EST
3"  Maintainer:  Benji Fisher PhD   <benji@member.AMS.org>
4"  Version:     1.13.2, for Vim 6.3+
5"  URL:		http://www.vim.org/script.php?script_id=39
6
7" Documentation:
8"  The documentation is in a separate file, matchit.txt .
9
10" Credits:
11"  Vim editor by Bram Moolenaar (Thanks, Bram!)
12"  Original script and design by Raul Segura Acevedo
13"  Support for comments by Douglas Potts
14"  Support for back references and other improvements by Benji Fisher
15"  Support for many languages by Johannes Zellner
16"  Suggestions for improvement, bug reports, and support for additional
17"  languages by Jordi-Albert Batalla, Neil Bird, Servatius Brandt, Mark
18"  Collett, Stephen Wall, Dany St-Amant, Yuheng Xie, and Johannes Zellner.
19
20" Debugging:
21"  If you'd like to try the built-in debugging commands...
22"   :MatchDebug      to activate debugging for the current buffer
23"  This saves the values of several key script variables as buffer-local
24"  variables.  See the MatchDebug() function, below, for details.
25
26" TODO:  I should think about multi-line patterns for b:match_words.
27"   This would require an option:  how many lines to scan (default 1).
28"   This would be useful for Python, maybe also for *ML.
29" TODO:  Maybe I should add a menu so that people will actually use some of
30"   the features that I have implemented.
31" TODO:  Eliminate the MultiMatch function.  Add yet another argument to
32"   Match_wrapper() instead.
33" TODO:  Allow :let b:match_words = '\(\(foo\)\(bar\)\):\3\2:end\1'
34" TODO:  Make backrefs safer by using '\V' (very no-magic).
35" TODO:  Add a level of indirection, so that custom % scripts can use my
36"   work but extend it.
37
38" allow user to prevent loading
39" and prevent duplicate loading
40if exists("loaded_matchit") || &cp
41  finish
42endif
43let loaded_matchit = 1
44let s:last_mps = ""
45let s:last_words = ":"
46
47let s:save_cpo = &cpo
48set cpo&vim
49
50nnoremap <silent> %  :<C-U>call <SID>Match_wrapper('',1,'n') <CR>
51nnoremap <silent> g% :<C-U>call <SID>Match_wrapper('',0,'n') <CR>
52vnoremap <silent> %  :<C-U>call <SID>Match_wrapper('',1,'v') <CR>m'gv``
53vnoremap <silent> g% :<C-U>call <SID>Match_wrapper('',0,'v') <CR>m'gv``
54onoremap <silent> %  v:<C-U>call <SID>Match_wrapper('',1,'o') <CR>
55onoremap <silent> g% v:<C-U>call <SID>Match_wrapper('',0,'o') <CR>
56
57" Analogues of [{ and ]} using matching patterns:
58nnoremap <silent> [% :<C-U>call <SID>MultiMatch("bW", "n") <CR>
59nnoremap <silent> ]% :<C-U>call <SID>MultiMatch("W",  "n") <CR>
60vmap [% <Esc>[%m'gv``
61vmap ]% <Esc>]%m'gv``
62" vnoremap <silent> [% :<C-U>call <SID>MultiMatch("bW", "v") <CR>m'gv``
63" vnoremap <silent> ]% :<C-U>call <SID>MultiMatch("W",  "v") <CR>m'gv``
64onoremap <silent> [% v:<C-U>call <SID>MultiMatch("bW", "o") <CR>
65onoremap <silent> ]% v:<C-U>call <SID>MultiMatch("W",  "o") <CR>
66
67" text object:
68vmap a% <Esc>[%v]%
69
70" Auto-complete mappings:  (not yet "ready for prime time")
71" TODO Read :help write-plugin for the "right" way to let the user
72" specify a key binding.
73"   let g:match_auto = '<C-]>'
74"   let g:match_autoCR = '<C-CR>'
75" if exists("g:match_auto")
76"   execute "inoremap " . g:match_auto . ' x<Esc>"=<SID>Autocomplete()<CR>Pls'
77" endif
78" if exists("g:match_autoCR")
79"   execute "inoremap " . g:match_autoCR . ' <CR><C-R>=<SID>Autocomplete()<CR>'
80" endif
81" if exists("g:match_gthhoh")
82"   execute "inoremap " . g:match_gthhoh . ' <C-O>:call <SID>Gthhoh()<CR>'
83" endif " gthhoh = "Get the heck out of here!"
84
85let s:notslash = '\\\@<!\%(\\\\\)*'
86
87function! s:Match_wrapper(word, forward, mode) range
88  " In s:CleanUp(), :execute "set" restore_options .
89  let restore_options = (&ic ? " " : " no") . "ignorecase"
90  if exists("b:match_ignorecase")
91    let &ignorecase = b:match_ignorecase
92  endif
93  let restore_options = " ve=" . &ve . restore_options
94  set ve=
95  " If this function was called from Visual mode, make sure that the cursor
96  " is at the correct end of the Visual range:
97  if a:mode == "v"
98    execute "normal! gv\<Esc>"
99  endif
100  " In s:CleanUp(), we may need to check whether the cursor moved forward.
101  let startline = line(".")
102  let startcol = col(".")
103  " Use default behavior if called with a count.
104  if v:count
105    exe "normal! " . v:count . "%"
106    return s:CleanUp(restore_options, a:mode, startline, startcol)
107  end
108
109  " First step:  if not already done, set the script variables
110  "   s:do_BR	flag for whether there are backrefs
111  "   s:pat	parsed version of b:match_words
112  "   s:all	regexp based on s:pat and the default groups
113  "
114  if !exists("b:match_words") || b:match_words == ""
115    let match_words = ""
116    " Allow b:match_words = "GetVimMatchWords()" .
117  elseif b:match_words =~ ":"
118    let match_words = b:match_words
119  else
120    execute "let match_words =" b:match_words
121  endif
122" Thanks to Preben "Peppe" Guldberg and Bram Moolenaar for this suggestion!
123  if (match_words != s:last_words) || (&mps != s:last_mps) ||
124    \ exists("b:match_debug")
125    let s:last_words = match_words
126    let s:last_mps = &mps
127    " The next several lines were here before
128    " BF started messing with this script.
129    " quote the special chars in 'matchpairs', replace [,:] with \| and then
130    " append the builtin pairs (/*, */, #if, #ifdef, #else, #elif, #endif)
131    " let default = substitute(escape(&mps, '[$^.*~\\/?]'), '[,:]\+',
132    "  \ '\\|', 'g').'\|\/\*\|\*\/\|#if\>\|#ifdef\>\|#else\>\|#elif\>\|#endif\>'
133    let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") .
134      \ '\/\*:\*\/,#if\%(def\)\=:#else\>:#elif\>:#endif\>'
135    " s:all = pattern with all the keywords
136    let match_words = match_words . (strlen(match_words) ? "," : "") . default
137    if match_words !~ s:notslash . '\\\d'
138      let s:do_BR = 0
139      let s:pat = match_words
140    else
141      let s:do_BR = 1
142      let s:pat = s:ParseWords(match_words)
143    endif
144    let s:all = substitute(s:pat, s:notslash . '\zs[,:]\+', '\\|', 'g')
145    let s:all = '\%(' . s:all . '\)'
146    " let s:all = '\%(' . substitute(s:all, '\\\ze[,:]', '', 'g') . '\)'
147    if exists("b:match_debug")
148      let b:match_pat = s:pat
149    endif
150  endif
151
152  " Second step:  set the following local variables:
153  "     matchline = line on which the cursor started
154  "     curcol    = number of characters before match
155  "     prefix    = regexp for start of line to start of match
156  "     suffix    = regexp for end of match to end of line
157  " Require match to end on or after the cursor and prefer it to
158  " start on or before the cursor.
159  let matchline = getline(startline)
160  if a:word != ''
161    " word given
162    if a:word !~ s:all
163      echohl WarningMsg|echo 'Missing rule for word:"'.a:word.'"'|echohl NONE
164      return s:CleanUp(restore_options, a:mode, startline, startcol)
165    endif
166    let matchline = a:word
167    let curcol = 0
168    let prefix = '^\%('
169    let suffix = '\)$'
170  " Now the case when "word" is not given
171  else	" Find the match that ends on or after the cursor and set curcol.
172    let regexp = s:Wholematch(matchline, s:all, startcol-1)
173    let curcol = match(matchline, regexp)
174    " If there is no match, give up.
175    if curcol == -1
176      return s:CleanUp(restore_options, a:mode, startline, startcol)
177    endif
178    let endcol = matchend(matchline, regexp)
179    let suf = strlen(matchline) - endcol
180    let prefix = (curcol ? '^.*\%'  . (curcol + 1) . 'c\%(' : '^\%(')
181    let suffix = (suf ? '\)\%' . (endcol + 1) . 'c.*$'  : '\)$')
182  endif
183  if exists("b:match_debug")
184    let b:match_match = matchstr(matchline, regexp)
185    let b:match_col = curcol+1
186  endif
187
188  " Third step:  Find the group and single word that match, and the original
189  " (backref) versions of these.  Then, resolve the backrefs.
190  " Set the following local variable:
191  " group = colon-separated list of patterns, one of which matches
192  "       = ini:mid:fin or ini:fin
193  "
194  " Reconstruct the version with unresolved backrefs.
195  let patBR = substitute(match_words.',',
196    \ s:notslash.'\zs[,:]*,[,:]*', ',', 'g')
197  let patBR = substitute(patBR, s:notslash.'\zs:\{2,}', ':', 'g')
198  " Now, set group and groupBR to the matching group: 'if:endif' or
199  " 'while:endwhile' or whatever.  A bit of a kluge:  s:Choose() returns
200  " group . "," . groupBR, and we pick it apart.
201  let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR)
202  let i = matchend(group, s:notslash . ",")
203  let groupBR = strpart(group, i)
204  let group = strpart(group, 0, i-1)
205  " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix
206  if s:do_BR " Do the hard part:  resolve those backrefs!
207    let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline)
208  endif
209  if exists("b:match_debug")
210    let b:match_wholeBR = groupBR
211    let i = matchend(groupBR, s:notslash . ":")
212    let b:match_iniBR = strpart(groupBR, 0, i-1)
213  endif
214
215  " Fourth step:  Set the arguments for searchpair().
216  let i = matchend(group, s:notslash . ":")
217  let j = matchend(group, '.*' . s:notslash . ":")
218  let ini = strpart(group, 0, i-1)
219  let mid = substitute(strpart(group, i,j-i-1), s:notslash.'\zs:', '\\|', 'g')
220  let fin = strpart(group, j)
221  "Un-escape the remaining , and : characters.
222  let ini = substitute(ini, s:notslash . '\zs\\\(:\|,\)', '\1', 'g')
223  let mid = substitute(mid, s:notslash . '\zs\\\(:\|,\)', '\1', 'g')
224  let fin = substitute(fin, s:notslash . '\zs\\\(:\|,\)', '\1', 'g')
225  " searchpair() requires that these patterns avoid \(\) groups.
226  let ini = substitute(ini, s:notslash . '\zs\\(', '\\%(', 'g')
227  let mid = substitute(mid, s:notslash . '\zs\\(', '\\%(', 'g')
228  let fin = substitute(fin, s:notslash . '\zs\\(', '\\%(', 'g')
229  " Set mid.  This is optimized for readability, not micro-efficiency!
230  if a:forward && matchline =~ prefix . fin . suffix
231    \ || !a:forward && matchline =~ prefix . ini . suffix
232    let mid = ""
233  endif
234  " Set flag.  This is optimized for readability, not micro-efficiency!
235  if a:forward && matchline =~ prefix . fin . suffix
236    \ || !a:forward && matchline !~ prefix . ini . suffix
237    let flag = "bW"
238  else
239    let flag = "W"
240  endif
241  " Set skip.
242  if exists("b:match_skip")
243    let skip = b:match_skip
244  elseif exists("b:match_comment") " backwards compatibility and testing!
245    let skip = "r:" . b:match_comment
246  else
247    let skip = 's:comment\|string'
248  endif
249  let skip = s:ParseSkip(skip)
250  if exists("b:match_debug")
251    let b:match_ini = ini
252    let b:match_tail = (strlen(mid) ? mid.'\|' : '') . fin
253  endif
254
255  " Fifth step:  actually start moving the cursor and call searchpair().
256  " Later, :execute restore_cursor to get to the original screen.
257  let restore_cursor = virtcol(".") . "|"
258  normal! g0
259  let restore_cursor = line(".") . "G" .  virtcol(".") . "|zs" . restore_cursor
260  normal! H
261  let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor
262  execute restore_cursor
263  call cursor(0, curcol + 1)
264  " normal! 0
265  " if curcol
266  "   execute "normal!" . curcol . "l"
267  " endif
268  if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on"))
269    let skip = "0"
270  else
271    execute "if " . skip . "| let skip = '0' | endif"
272  endif
273  let sp_return = searchpair(ini, mid, fin, flag, skip)
274  let final_position = "call cursor(" . line(".") . "," . col(".") . ")"
275  " Restore cursor position and original screen.
276  execute restore_cursor
277  normal! m'
278  if sp_return > 0
279    execute final_position
280  endif
281  return s:CleanUp(restore_options, a:mode, startline, startcol, mid.'\|'.fin)
282endfun
283
284" Restore options and do some special handling for Operator-pending mode.
285" The optional argument is the tail of the matching group.
286fun! s:CleanUp(options, mode, startline, startcol, ...)
287  execute "set" a:options
288  " Open folds, if appropriate.
289  if a:mode != "o"
290    if &foldopen =~ "percent"
291      normal! zv
292    endif
293    " In Operator-pending mode, we want to include the whole match
294    " (for example, d%).
295    " This is only a problem if we end up moving in the forward direction.
296  elseif (a:startline < line(".")) ||
297	\ (a:startline == line(".") && a:startcol < col("."))
298    if a:0
299      " Check whether the match is a single character.  If not, move to the
300      " end of the match.
301      let matchline = getline(".")
302      let currcol = col(".")
303      let regexp = s:Wholematch(matchline, a:1, currcol-1)
304      let endcol = matchend(matchline, regexp)
305      if endcol > currcol  " This is NOT off by one!
306	execute "normal!" . (endcol - currcol) . "l"
307      endif
308    endif " a:0
309  endif " a:mode != "o" && etc.
310  return 0
311endfun
312
313" Example (simplified HTML patterns):  if
314"   a:groupBR	= '<\(\k\+\)>:</\1>'
315"   a:prefix	= '^.\{3}\('
316"   a:group	= '<\(\k\+\)>:</\(\k\+\)>'
317"   a:suffix	= '\).\{2}$'
318"   a:matchline	=  "123<tag>12" or "123</tag>12"
319" then extract "tag" from a:matchline and return "<tag>:</tag>" .
320fun! s:InsertRefs(groupBR, prefix, group, suffix, matchline)
321  if a:matchline !~ a:prefix .
322    \ substitute(a:group, s:notslash . '\zs:', '\\|', 'g') . a:suffix
323    return a:group
324  endif
325  let i = matchend(a:groupBR, s:notslash . ':')
326  let ini = strpart(a:groupBR, 0, i-1)
327  let tailBR = strpart(a:groupBR, i)
328  let word = s:Choose(a:group, a:matchline, ":", "", a:prefix, a:suffix,
329    \ a:groupBR)
330  let i = matchend(word, s:notslash . ":")
331  let wordBR = strpart(word, i)
332  let word = strpart(word, 0, i-1)
333  " Now, a:matchline =~ a:prefix . word . a:suffix
334  if wordBR != ini
335    let table = s:Resolve(ini, wordBR, "table")
336  else
337    " let table = "----------"
338    let table = ""
339    let d = 0
340    while d < 10
341      if tailBR =~ s:notslash . '\\' . d
342	" let table[d] = d
343	let table = table . d
344      else
345	let table = table . "-"
346      endif
347      let d = d + 1
348    endwhile
349  endif
350  let d = 9
351  while d
352    if table[d] != "-"
353      let backref = substitute(a:matchline, a:prefix.word.a:suffix,
354	\ '\'.table[d], "")
355	" Are there any other characters that should be escaped?
356      let backref = escape(backref, '*,:')
357      execute s:Ref(ini, d, "start", "len")
358      let ini = strpart(ini, 0, start) . backref . strpart(ini, start+len)
359      let tailBR = substitute(tailBR, s:notslash . '\zs\\' . d,
360	\ escape(backref, '\\&'), 'g')
361    endif
362    let d = d-1
363  endwhile
364  if exists("b:match_debug")
365    if s:do_BR
366      let b:match_table = table
367      let b:match_word = word
368    else
369      let b:match_table = ""
370      let b:match_word = ""
371    endif
372  endif
373  return ini . ":" . tailBR
374endfun
375
376" Input a comma-separated list of groups with backrefs, such as
377"   a:groups = '\(foo\):end\1,\(bar\):end\1'
378" and return a comma-separated list of groups with backrefs replaced:
379"   return '\(foo\):end\(foo\),\(bar\):end\(bar\)'
380fun! s:ParseWords(groups)
381  let groups = substitute(a:groups.",", s:notslash.'\zs[,:]*,[,:]*', ',', 'g')
382  let groups = substitute(groups, s:notslash . '\zs:\{2,}', ':', 'g')
383  let parsed = ""
384  while groups =~ '[^,:]'
385    let i = matchend(groups, s:notslash . ':')
386    let j = matchend(groups, s:notslash . ',')
387    let ini = strpart(groups, 0, i-1)
388    let tail = strpart(groups, i, j-i-1) . ":"
389    let groups = strpart(groups, j)
390    let parsed = parsed . ini
391    let i = matchend(tail, s:notslash . ':')
392    while i != -1
393      " In 'if:else:endif', ini='if' and word='else' and then word='endif'.
394      let word = strpart(tail, 0, i-1)
395      let tail = strpart(tail, i)
396      let i = matchend(tail, s:notslash . ':')
397      let parsed = parsed . ":" . s:Resolve(ini, word, "word")
398    endwhile " Now, tail has been used up.
399    let parsed = parsed . ","
400  endwhile " groups =~ '[^,:]'
401  let parsed = substitute(parsed, ',$', '', '')
402  return parsed
403endfun
404
405" TODO I think this can be simplified and/or made more efficient.
406" TODO What should I do if a:start is out of range?
407" Return a regexp that matches all of a:string, such that
408" matchstr(a:string, regexp) represents the match for a:pat that starts
409" as close to a:start as possible, before being preferred to after, and
410" ends after a:start .
411" Usage:
412" let regexp = s:Wholematch(getline("."), 'foo\|bar', col(".")-1)
413" let i      = match(getline("."), regexp)
414" let j      = matchend(getline("."), regexp)
415" let match  = matchstr(getline("."), regexp)
416fun! s:Wholematch(string, pat, start)
417  let group = '\%(' . a:pat . '\)'
418  let prefix = (a:start ? '\(^.*\%<' . (a:start + 2) . 'c\)\zs' : '^')
419  let len = strlen(a:string)
420  let suffix = (a:start+1 < len ? '\(\%>'.(a:start+1).'c.*$\)\@=' : '$')
421  if a:string !~ prefix . group . suffix
422    let prefix = ''
423  endif
424  return prefix . group . suffix
425endfun
426
427" No extra arguments:  s:Ref(string, d) will
428" find the d'th occurrence of '\(' and return it, along with everything up
429" to and including the matching '\)'.
430" One argument:  s:Ref(string, d, "start") returns the index of the start
431" of the d'th '\(' and any other argument returns the length of the group.
432" Two arguments:  s:Ref(string, d, "foo", "bar") returns a string to be
433" executed, having the effect of
434"   :let foo = s:Ref(string, d, "start")
435"   :let bar = s:Ref(string, d, "len")
436fun! s:Ref(string, d, ...)
437  let len = strlen(a:string)
438  if a:d == 0
439    let start = 0
440  else
441    let cnt = a:d
442    let match = a:string
443    while cnt
444      let cnt = cnt - 1
445      let index = matchend(match, s:notslash . '\\(')
446      if index == -1
447	return ""
448      endif
449      let match = strpart(match, index)
450    endwhile
451    let start = len - strlen(match)
452    if a:0 == 1 && a:1 == "start"
453      return start - 2
454    endif
455    let cnt = 1
456    while cnt
457      let index = matchend(match, s:notslash . '\\(\|\\)') - 1
458      if index == -2
459	return ""
460      endif
461      " Increment if an open, decrement if a ')':
462      let cnt = cnt + (match[index]=="(" ? 1 : -1)  " ')'
463      " let cnt = stridx('0(', match[index]) + cnt
464      let match = strpart(match, index+1)
465    endwhile
466    let start = start - 2
467    let len = len - start - strlen(match)
468  endif
469  if a:0 == 1
470    return len
471  elseif a:0 == 2
472    return "let " . a:1 . "=" . start . "| let " . a:2 . "=" . len
473  else
474    return strpart(a:string, start, len)
475  endif
476endfun
477
478" Count the number of disjoint copies of pattern in string.
479" If the pattern is a literal string and contains no '0' or '1' characters
480" then s:Count(string, pattern, '0', '1') should be faster than
481" s:Count(string, pattern).
482fun! s:Count(string, pattern, ...)
483  let pat = escape(a:pattern, '\\')
484  if a:0 > 1
485    let foo = substitute(a:string, '[^'.a:pattern.']', "a:1", "g")
486    let foo = substitute(a:string, pat, a:2, "g")
487    let foo = substitute(foo, '[^' . a:2 . ']', "", "g")
488    return strlen(foo)
489  endif
490  let result = 0
491  let foo = a:string
492  let index = matchend(foo, pat)
493  while index != -1
494    let result = result + 1
495    let foo = strpart(foo, index)
496    let index = matchend(foo, pat)
497  endwhile
498  return result
499endfun
500
501" s:Resolve('\(a\)\(b\)', '\(c\)\2\1\1\2') should return table.word, where
502" word = '\(c\)\(b\)\(a\)\3\2' and table = '-32-------'.  That is, the first
503" '\1' in target is replaced by '\(a\)' in word, table[1] = 3, and this
504" indicates that all other instances of '\1' in target are to be replaced
505" by '\3'.  The hard part is dealing with nesting...
506" Note that ":" is an illegal character for source and target,
507" unless it is preceded by "\".
508fun! s:Resolve(source, target, output)
509  let word = a:target
510  let i = matchend(word, s:notslash . '\\\d') - 1
511  let table = "----------"
512  while i != -2 " There are back references to be replaced.
513    let d = word[i]
514    let backref = s:Ref(a:source, d)
515    " The idea is to replace '\d' with backref.  Before we do this,
516    " replace any \(\) groups in backref with :1, :2, ... if they
517    " correspond to the first, second, ... group already inserted
518    " into backref.  Later, replace :1 with \1 and so on.  The group
519    " number w+b within backref corresponds to the group number
520    " s within a:source.
521    " w = number of '\(' in word before the current one
522    let w = s:Count(
523    \ substitute(strpart(word, 0, i-1), '\\\\', '', 'g'), '\(', '1')
524    let b = 1 " number of the current '\(' in backref
525    let s = d " number of the current '\(' in a:source
526    while b <= s:Count(substitute(backref, '\\\\', '', 'g'), '\(', '1')
527    \ && s < 10
528      if table[s] == "-"
529	if w + b < 10
530	  " let table[s] = w + b
531	  let table = strpart(table, 0, s) . (w+b) . strpart(table, s+1)
532	endif
533	let b = b + 1
534	let s = s + 1
535      else
536	execute s:Ref(backref, b, "start", "len")
537	let ref = strpart(backref, start, len)
538	let backref = strpart(backref, 0, start) . ":". table[s]
539	\ . strpart(backref, start+len)
540	let s = s + s:Count(substitute(ref, '\\\\', '', 'g'), '\(', '1')
541      endif
542    endwhile
543    let word = strpart(word, 0, i-1) . backref . strpart(word, i+1)
544    let i = matchend(word, s:notslash . '\\\d') - 1
545  endwhile
546  let word = substitute(word, s:notslash . '\zs:', '\\', 'g')
547  if a:output == "table"
548    return table
549  elseif a:output == "word"
550    return word
551  else
552    return table . word
553  endif
554endfun
555
556" Assume a:comma = ",".  Then the format for a:patterns and a:1 is
557"   a:patterns = "<pat1>,<pat2>,..."
558"   a:1 = "<alt1>,<alt2>,..."
559" If <patn> is the first pattern that matches a:string then return <patn>
560" if no optional arguments are given; return <patn>,<altn> if a:1 is given.
561fun! s:Choose(patterns, string, comma, branch, prefix, suffix, ...)
562  let tail = (a:patterns =~ a:comma."$" ? a:patterns : a:patterns . a:comma)
563  let i = matchend(tail, s:notslash . a:comma)
564  if a:0
565    let alttail = (a:1 =~ a:comma."$" ? a:1 : a:1 . a:comma)
566    let j = matchend(alttail, s:notslash . a:comma)
567  endif
568  let current = strpart(tail, 0, i-1)
569  if a:branch == ""
570    let currpat = current
571  else
572    let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g')
573  endif
574  while a:string !~ a:prefix . currpat . a:suffix
575    let tail = strpart(tail, i)
576    let i = matchend(tail, s:notslash . a:comma)
577    if i == -1
578      return -1
579    endif
580    let current = strpart(tail, 0, i-1)
581    if a:branch == ""
582      let currpat = current
583    else
584      let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g')
585    endif
586    if a:0
587      let alttail = strpart(alttail, j)
588      let j = matchend(alttail, s:notslash . a:comma)
589    endif
590  endwhile
591  if a:0
592    let current = current . a:comma . strpart(alttail, 0, j-1)
593  endif
594  return current
595endfun
596
597" Call this function to turn on debugging information.  Every time the main
598" script is run, buffer variables will be saved.  These can be used directly
599" or viewed using the menu items below.
600if !exists(":MatchDebug")
601  command! -nargs=0 MatchDebug call s:Match_debug()
602endif
603
604fun! s:Match_debug()
605  let b:match_debug = 1	" Save debugging information.
606  " pat = all of b:match_words with backrefs parsed
607  amenu &Matchit.&pat	:echo b:match_pat<CR>
608  " match = bit of text that is recognized as a match
609  amenu &Matchit.&match	:echo b:match_match<CR>
610  " curcol = cursor column of the start of the matching text
611  amenu &Matchit.&curcol	:echo b:match_col<CR>
612  " wholeBR = matching group, original version
613  amenu &Matchit.wh&oleBR	:echo b:match_wholeBR<CR>
614  " iniBR = 'if' piece, original version
615  amenu &Matchit.ini&BR	:echo b:match_iniBR<CR>
616  " ini = 'if' piece, with all backrefs resolved from match
617  amenu &Matchit.&ini	:echo b:match_ini<CR>
618  " tail = 'else\|endif' piece, with all backrefs resolved from match
619  amenu &Matchit.&tail	:echo b:match_tail<CR>
620  " fin = 'endif' piece, with all backrefs resolved from match
621  amenu &Matchit.&word	:echo b:match_word<CR>
622  " '\'.d in ini refers to the same thing as '\'.table[d] in word.
623  amenu &Matchit.t&able	:echo '0:' . b:match_table . ':9'<CR>
624endfun
625
626" Jump to the nearest unmatched "(" or "if" or "<tag>" if a:spflag == "bW"
627" or the nearest unmatched "</tag>" or "endif" or ")" if a:spflag == "W".
628" Return a "mark" for the original position, so that
629"   let m = MultiMatch("bW", "n") ... execute m
630" will return to the original position.  If there is a problem, do not
631" move the cursor and return "", unless a count is given, in which case
632" go up or down as many levels as possible and again return "".
633" TODO This relies on the same patterns as % matching.  It might be a good
634" idea to give it its own matching patterns.
635fun! s:MultiMatch(spflag, mode)
636  if !exists("b:match_words") || b:match_words == ""
637    return ""
638  end
639  let restore_options = (&ic ? "" : "no") . "ignorecase"
640  if exists("b:match_ignorecase")
641    let &ignorecase = b:match_ignorecase
642  endif
643  let startline = line(".")
644  let startcol = col(".")
645
646  " First step:  if not already done, set the script variables
647  "   s:do_BR	flag for whether there are backrefs
648  "   s:pat	parsed version of b:match_words
649  "   s:all	regexp based on s:pat and the default groups
650  " This part is copied and slightly modified from s:Match_wrapper().
651  let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") .
652    \ '\/\*:\*\/,#if\%(def\)\=:#else\>:#elif\>:#endif\>'
653  " Allow b:match_words = "GetVimMatchWords()" .
654  if b:match_words =~ ":"
655    let match_words = b:match_words
656  else
657    execute "let match_words =" b:match_words
658  endif
659  if (match_words != s:last_words) || (&mps != s:last_mps) ||
660    \ exists("b:match_debug")
661    let s:last_words = match_words
662    let s:last_mps = &mps
663    if match_words !~ s:notslash . '\\\d'
664      let s:do_BR = 0
665      let s:pat = match_words
666    else
667      let s:do_BR = 1
668      let s:pat = s:ParseWords(match_words)
669    endif
670    let s:all = '\%(' . substitute(s:pat . (strlen(s:pat)?",":"") . default,
671      \	'[,:]\+','\\|','g') . '\)'
672    if exists("b:match_debug")
673      let b:match_pat = s:pat
674    endif
675  endif
676
677  " Second step:  figure out the patterns for searchpair()
678  " and save the screen, cursor position, and 'ignorecase'.
679  " - TODO:  A lot of this is copied from s:Match_wrapper().
680  " - maybe even more functionality should be split off
681  " - into separate functions!
682  let cdefault = (s:pat =~ '[^,]$' ? "," : "") . default
683  let open =  substitute(s:pat . cdefault,
684	\ s:notslash . '\zs:.\{-}' . s:notslash . ',', '\\),\\(', 'g')
685  let open =  '\(' . substitute(open, s:notslash . '\zs:.*$', '\\)', '')
686  let close = substitute(s:pat . cdefault,
687	\ s:notslash . '\zs,.\{-}' . s:notslash . ':', '\\),\\(', 'g')
688  let close = substitute(close, '^.\{-}' . s:notslash . ':', '\\(', '') . '\)'
689  if exists("b:match_skip")
690    let skip = b:match_skip
691  elseif exists("b:match_comment") " backwards compatibility and testing!
692    let skip = "r:" . b:match_comment
693  else
694    let skip = 's:comment\|string'
695  endif
696  let skip = s:ParseSkip(skip)
697  " let restore_cursor = line(".") . "G" . virtcol(".") . "|"
698  " normal! H
699  " let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor
700  let restore_cursor = virtcol(".") . "|"
701  normal! g0
702  let restore_cursor = line(".") . "G" .  virtcol(".") . "|zs" . restore_cursor
703  normal! H
704  let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor
705  execute restore_cursor
706
707  " Third step: call searchpair().
708  " Replace '\('--but not '\\('--with '\%(' and ',' with '\|'.
709  let openpat =  substitute(open, '\(\\\@<!\(\\\\\)*\)\@<=\\(', '\\%(', 'g')
710  let openpat = substitute(openpat, ',', '\\|', 'g')
711  let closepat = substitute(close, '\(\\\@<!\(\\\\\)*\)\@<=\\(', '\\%(', 'g')
712  let closepat = substitute(closepat, ',', '\\|', 'g')
713  if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on"))
714    let skip = '0'
715  else
716    execute "if " . skip . "| let skip = '0' | endif"
717  endif
718  mark '
719  let level = v:count1
720  while level
721    if searchpair(openpat, '', closepat, a:spflag, skip) < 1
722      call s:CleanUp(restore_options, a:mode, startline, startcol)
723      return ""
724    endif
725    let level = level - 1
726  endwhile
727
728  " Restore options and return a string to restore the original position.
729  call s:CleanUp(restore_options, a:mode, startline, startcol)
730  return restore_cursor
731endfun
732
733" Search backwards for "if" or "while" or "<tag>" or ...
734" and return "endif" or "endwhile" or "</tag>" or ... .
735" For now, this uses b:match_words and the same script variables
736" as s:Match_wrapper() .  Later, it may get its own patterns,
737" either from a buffer variable or passed as arguments.
738" fun! s:Autocomplete()
739"   echo "autocomplete not yet implemented :-("
740"   if !exists("b:match_words") || b:match_words == ""
741"     return ""
742"   end
743"   let startpos = s:MultiMatch("bW")
744"
745"   if startpos == ""
746"     return ""
747"   endif
748"   " - TODO:  figure out whether 'if' or '<tag>' matched, and construct
749"   " - the appropriate closing.
750"   let matchline = getline(".")
751"   let curcol = col(".") - 1
752"   " - TODO:  Change the s:all argument if there is a new set of match pats.
753"   let regexp = s:Wholematch(matchline, s:all, curcol)
754"   let suf = strlen(matchline) - matchend(matchline, regexp)
755"   let prefix = (curcol ? '^.\{'  . curcol . '}\%(' : '^\%(')
756"   let suffix = (suf ? '\).\{' . suf . '}$'  : '\)$')
757"   " Reconstruct the version with unresolved backrefs.
758"   let patBR = substitute(b:match_words.',', '[,:]*,[,:]*', ',', 'g')
759"   let patBR = substitute(patBR, ':\{2,}', ':', "g")
760"   " Now, set group and groupBR to the matching group: 'if:endif' or
761"   " 'while:endwhile' or whatever.
762"   let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR)
763"   let i = matchend(group, s:notslash . ",")
764"   let groupBR = strpart(group, i)
765"   let group = strpart(group, 0, i-1)
766"   " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix
767"   if s:do_BR
768"     let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline)
769"   endif
770" " let g:group = group
771"
772"   " - TODO:  Construct the closing from group.
773"   let fake = "end" . expand("<cword>")
774"   execute startpos
775"   return fake
776" endfun
777
778" Close all open structures.  "Get the heck out of here!"
779" fun! s:Gthhoh()
780"   let close = s:Autocomplete()
781"   while strlen(close)
782"     put=close
783"     let close = s:Autocomplete()
784"   endwhile
785" endfun
786
787" Parse special strings as typical skip arguments for searchpair():
788"   s:foo becomes (current syntax item) =~ foo
789"   S:foo becomes (current syntax item) !~ foo
790"   r:foo becomes (line before cursor) =~ foo
791"   R:foo becomes (line before cursor) !~ foo
792fun! s:ParseSkip(str)
793  let skip = a:str
794  if skip[1] == ":"
795    if skip[0] == "s"
796      let skip = "synIDattr(synID(line('.'),col('.'),1),'name') =~? '" .
797	\ strpart(skip,2) . "'"
798    elseif skip[0] == "S"
799      let skip = "synIDattr(synID(line('.'),col('.'),1),'name') !~? '" .
800	\ strpart(skip,2) . "'"
801    elseif skip[0] == "r"
802      let skip = "strpart(getline('.'),0,col('.'))=~'" . strpart(skip,2). "'"
803    elseif skip[0] == "R"
804      let skip = "strpart(getline('.'),0,col('.'))!~'" . strpart(skip,2). "'"
805    endif
806  endif
807  return skip
808endfun
809
810let &cpo = s:save_cpo
811
812" vim:sts=2:sw=2:
813