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