Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 1 | " matchit.vim: (global plugin) Extended "%" matching |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 2 | " Last Change: Mon May 15 10:00 PM 2006 EDT |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 3 | " Maintainer: Benji Fisher PhD <benji@member.AMS.org> |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 4 | " Version: 1.11, for Vim 6.3+ |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 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 |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 18 | " Collett, Stephen Wall, Dany St-Amant, Yuheng Xie, and Johannes Zellner. |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 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 |
| 40 | if exists("loaded_matchit") || &cp |
| 41 | finish |
| 42 | endif |
| 43 | let loaded_matchit = 1 |
| 44 | let s:last_mps = "" |
| 45 | let s:last_words = "" |
| 46 | |
| 47 | let s:save_cpo = &cpo |
| 48 | set cpo&vim |
| 49 | |
| 50 | nnoremap <silent> % :<C-U>call <SID>Match_wrapper('',1,'n') <CR> |
| 51 | nnoremap <silent> g% :<C-U>call <SID>Match_wrapper('',0,'n') <CR> |
| 52 | vnoremap <silent> % :<C-U>call <SID>Match_wrapper('',1,'v') <CR>m'gv`` |
| 53 | vnoremap <silent> g% :<C-U>call <SID>Match_wrapper('',0,'v') <CR>m'gv`` |
| 54 | onoremap <silent> % v:<C-U>call <SID>Match_wrapper('',1,'o') <CR> |
| 55 | onoremap <silent> g% v:<C-U>call <SID>Match_wrapper('',0,'o') <CR> |
| 56 | |
| 57 | " Analogues of [{ and ]} using matching patterns: |
| 58 | nnoremap <silent> [% :<C-U>call <SID>MultiMatch("bW", "n") <CR> |
| 59 | nnoremap <silent> ]% :<C-U>call <SID>MultiMatch("W", "n") <CR> |
| 60 | vmap [% <Esc>[%m'gv`` |
| 61 | vmap ]% <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`` |
| 64 | onoremap <silent> [% v:<C-U>call <SID>MultiMatch("bW", "o") <CR> |
| 65 | onoremap <silent> ]% v:<C-U>call <SID>MultiMatch("W", "o") <CR> |
| 66 | |
| 67 | " text object: |
| 68 | vmap 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 | |
| 85 | let s:notslash = '\\\@<!\%(\\\\\)*' |
| 86 | |
| 87 | function! 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 or if no patterns are defined. |
| 104 | if v:count |
| 105 | exe "normal! " . v:count . "%" |
| 106 | return s:CleanUp(restore_options, a:mode, startline, startcol) |
| 107 | elseif !exists("b:match_words") || b:match_words == "" |
| 108 | silent! normal! % |
| 109 | return s:CleanUp(restore_options, a:mode, startline, startcol) |
| 110 | end |
| 111 | |
| 112 | " First step: if not already done, set the script variables |
| 113 | " s:do_BR flag for whether there are backrefs |
| 114 | " s:pat parsed version of b:match_words |
| 115 | " s:all regexp based on s:pat and the default groups |
| 116 | " |
| 117 | " Allow b:match_words = "GetVimMatchWords()" . |
| 118 | if b:match_words =~ ":" |
| 119 | let match_words = b:match_words |
| 120 | else |
| 121 | execute "let match_words =" b:match_words |
| 122 | endif |
| 123 | " Thanks to Preben "Peppe" Guldberg and Bram Moolenaar for this suggestion! |
| 124 | if (match_words != s:last_words) || (&mps != s:last_mps) || |
| 125 | \ exists("b:match_debug") |
| 126 | let s:last_words = match_words |
| 127 | let s:last_mps = &mps |
| 128 | if match_words !~ s:notslash . '\\\d' |
| 129 | let s:do_BR = 0 |
| 130 | let s:pat = match_words |
| 131 | else |
| 132 | let s:do_BR = 1 |
| 133 | let s:pat = s:ParseWords(match_words) |
| 134 | endif |
| 135 | " The next several lines were here before |
| 136 | " BF started messing with this script. |
| 137 | " quote the special chars in 'matchpairs', replace [,:] with \| and then |
| 138 | " append the builtin pairs (/*, */, #if, #ifdef, #else, #elif, #endif) |
| 139 | " let default = substitute(escape(&mps, '[$^.*~\\/?]'), '[,:]\+', |
| 140 | " \ '\\|', 'g').'\|\/\*\|\*\/\|#if\>\|#ifdef\>\|#else\>\|#elif\>\|#endif\>' |
| 141 | let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") . |
| 142 | \ '\/\*:\*\/,#if\%(def\)\=:#else\>:#elif\>:#endif\>' |
| 143 | " s:all = pattern with all the keywords |
| 144 | let s:all = s:pat . (strlen(s:pat) ? "," : "") . default |
| 145 | let s:all = substitute(s:all, s:notslash . '\zs[,:]\+', '\\|', 'g') |
| 146 | let s:all = '\%(' . s:all . '\)' |
| 147 | " let s:all = '\%(' . substitute(s:all, '\\\ze[,:]', '', 'g') . '\)' |
| 148 | if exists("b:match_debug") |
| 149 | let b:match_pat = s:pat |
| 150 | endif |
| 151 | endif |
| 152 | |
| 153 | " Second step: set the following local variables: |
| 154 | " matchline = line on which the cursor started |
| 155 | " curcol = number of characters before match |
| 156 | " prefix = regexp for start of line to start of match |
| 157 | " suffix = regexp for end of match to end of line |
| 158 | " Require match to end on or after the cursor and prefer it to |
| 159 | " start on or before the cursor. |
| 160 | let matchline = getline(startline) |
| 161 | if a:word != '' |
| 162 | " word given |
| 163 | if a:word !~ s:all |
| 164 | echohl WarningMsg|echo 'Missing rule for word:"'.a:word.'"'|echohl NONE |
| 165 | return s:CleanUp(restore_options, a:mode, startline, startcol) |
| 166 | endif |
| 167 | let matchline = a:word |
| 168 | let curcol = 0 |
| 169 | let prefix = '^\%(' |
| 170 | let suffix = '\)$' |
| 171 | " Now the case when "word" is not given |
| 172 | else " Find the match that ends on or after the cursor and set curcol. |
| 173 | let regexp = s:Wholematch(matchline, s:all, startcol-1) |
| 174 | let curcol = match(matchline, regexp) |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 175 | let endcol = matchend(matchline, regexp) |
| 176 | let suf = strlen(matchline) - endcol |
| 177 | let prefix = (curcol ? '^.*\%' . (curcol + 1) . 'c\%(' : '^\%(') |
| 178 | let suffix = (suf ? '\)\%' . (endcol + 1) . 'c.*$' : '\)$') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 179 | " If the match comes from the defaults, bail out. |
| 180 | if matchline !~ prefix . |
| 181 | \ substitute(s:pat, s:notslash.'\zs[,:]\+', '\\|', 'g') . suffix |
| 182 | silent! norm! % |
| 183 | return s:CleanUp(restore_options, a:mode, startline, startcol) |
| 184 | endif |
| 185 | endif |
| 186 | if exists("b:match_debug") |
| 187 | let b:match_match = matchstr(matchline, regexp) |
| 188 | let b:match_col = curcol+1 |
| 189 | endif |
| 190 | |
| 191 | " Third step: Find the group and single word that match, and the original |
| 192 | " (backref) versions of these. Then, resolve the backrefs. |
| 193 | " Set the following local variable: |
| 194 | " group = colon-separated list of patterns, one of which matches |
| 195 | " = ini:mid:fin or ini:fin |
| 196 | " |
| 197 | " Reconstruct the version with unresolved backrefs. |
| 198 | let patBR = substitute(match_words.',', |
| 199 | \ s:notslash.'\zs[,:]*,[,:]*', ',', 'g') |
| 200 | let patBR = substitute(patBR, s:notslash.'\zs:\{2,}', ':', 'g') |
| 201 | " Now, set group and groupBR to the matching group: 'if:endif' or |
| 202 | " 'while:endwhile' or whatever. A bit of a kluge: s:Choose() returns |
| 203 | " group . "," . groupBR, and we pick it apart. |
| 204 | let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR) |
| 205 | let i = matchend(group, s:notslash . ",") |
| 206 | let groupBR = strpart(group, i) |
| 207 | let group = strpart(group, 0, i-1) |
| 208 | " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix |
| 209 | if s:do_BR " Do the hard part: resolve those backrefs! |
| 210 | let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline) |
| 211 | endif |
| 212 | if exists("b:match_debug") |
| 213 | let b:match_wholeBR = groupBR |
| 214 | let i = matchend(groupBR, s:notslash . ":") |
| 215 | let b:match_iniBR = strpart(groupBR, 0, i-1) |
| 216 | endif |
| 217 | |
| 218 | " Fourth step: Set the arguments for searchpair(). |
| 219 | let i = matchend(group, s:notslash . ":") |
| 220 | let j = matchend(group, '.*' . s:notslash . ":") |
| 221 | let ini = strpart(group, 0, i-1) |
| 222 | let mid = substitute(strpart(group, i,j-i-1), s:notslash.'\zs:', '\\|', 'g') |
| 223 | let fin = strpart(group, j) |
Bram Moolenaar | 76b92b2 | 2006-03-24 22:46:53 +0000 | [diff] [blame] | 224 | "Un-escape the remaining , and : characters. |
| 225 | let ini = substitute(ini, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') |
| 226 | let mid = substitute(mid, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') |
| 227 | let fin = substitute(fin, s:notslash . '\zs\\\(:\|,\)', '\1', 'g') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 228 | " searchpair() requires that these patterns avoid \(\) groups. |
| 229 | let ini = substitute(ini, s:notslash . '\zs\\(', '\\%(', 'g') |
| 230 | let mid = substitute(mid, s:notslash . '\zs\\(', '\\%(', 'g') |
| 231 | let fin = substitute(fin, s:notslash . '\zs\\(', '\\%(', 'g') |
| 232 | " Set mid. This is optimized for readability, not micro-efficiency! |
| 233 | if a:forward && matchline =~ prefix . fin . suffix |
| 234 | \ || !a:forward && matchline =~ prefix . ini . suffix |
| 235 | let mid = "" |
| 236 | endif |
| 237 | " Set flag. This is optimized for readability, not micro-efficiency! |
| 238 | if a:forward && matchline =~ prefix . fin . suffix |
| 239 | \ || !a:forward && matchline !~ prefix . ini . suffix |
| 240 | let flag = "bW" |
| 241 | else |
| 242 | let flag = "W" |
| 243 | endif |
| 244 | " Set skip. |
| 245 | if exists("b:match_skip") |
| 246 | let skip = b:match_skip |
| 247 | elseif exists("b:match_comment") " backwards compatibility and testing! |
| 248 | let skip = "r:" . b:match_comment |
| 249 | else |
| 250 | let skip = 's:comment\|string' |
| 251 | endif |
| 252 | let skip = s:ParseSkip(skip) |
| 253 | if exists("b:match_debug") |
| 254 | let b:match_ini = ini |
| 255 | let b:match_tail = (strlen(mid) ? mid.'\|' : '') . fin |
| 256 | endif |
| 257 | |
| 258 | " Fifth step: actually start moving the cursor and call searchpair(). |
| 259 | " Later, :execute restore_cursor to get to the original screen. |
| 260 | let restore_cursor = virtcol(".") . "|" |
| 261 | normal! g0 |
| 262 | let restore_cursor = line(".") . "G" . virtcol(".") . "|zs" . restore_cursor |
| 263 | normal! H |
| 264 | let restore_cursor = "normal!" . line(".") . "Gzt" . restore_cursor |
| 265 | execute restore_cursor |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 266 | call cursor(0, curcol + 1) |
| 267 | " normal! 0 |
| 268 | " if curcol |
| 269 | " execute "normal!" . curcol . "l" |
| 270 | " endif |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 271 | if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on")) |
| 272 | let skip = "0" |
| 273 | else |
| 274 | execute "if " . skip . "| let skip = '0' | endif" |
| 275 | endif |
| 276 | let sp_return = searchpair(ini, mid, fin, flag, skip) |
| 277 | let final_position = "call cursor(" . line(".") . "," . col(".") . ")" |
| 278 | " Restore cursor position and original screen. |
| 279 | execute restore_cursor |
| 280 | normal! m' |
| 281 | if sp_return > 0 |
| 282 | execute final_position |
| 283 | endif |
| 284 | return s:CleanUp(restore_options, a:mode, startline, startcol, mid.'\|'.fin) |
| 285 | endfun |
| 286 | |
| 287 | " Restore options and do some special handling for Operator-pending mode. |
| 288 | " The optional argument is the tail of the matching group. |
| 289 | fun! s:CleanUp(options, mode, startline, startcol, ...) |
| 290 | execute "set" a:options |
| 291 | " Open folds, if appropriate. |
| 292 | if a:mode != "o" |
| 293 | if &foldopen =~ "percent" |
| 294 | normal! zv |
| 295 | endif |
| 296 | " In Operator-pending mode, we want to include the whole match |
| 297 | " (for example, d%). |
| 298 | " This is only a problem if we end up moving in the forward direction. |
| 299 | elseif (a:startline < line(".")) || |
| 300 | \ (a:startline == line(".") && a:startcol < col(".")) |
| 301 | if a:0 |
| 302 | " Check whether the match is a single character. If not, move to the |
| 303 | " end of the match. |
| 304 | let matchline = getline(".") |
| 305 | let currcol = col(".") |
| 306 | let regexp = s:Wholematch(matchline, a:1, currcol-1) |
| 307 | let endcol = matchend(matchline, regexp) |
| 308 | if endcol > currcol " This is NOT off by one! |
| 309 | execute "normal!" . (endcol - currcol) . "l" |
| 310 | endif |
| 311 | endif " a:0 |
| 312 | endif " a:mode != "o" && etc. |
| 313 | return 0 |
| 314 | endfun |
| 315 | |
| 316 | " Example (simplified HTML patterns): if |
| 317 | " a:groupBR = '<\(\k\+\)>:</\1>' |
| 318 | " a:prefix = '^.\{3}\(' |
| 319 | " a:group = '<\(\k\+\)>:</\(\k\+\)>' |
| 320 | " a:suffix = '\).\{2}$' |
| 321 | " a:matchline = "123<tag>12" or "123</tag>12" |
| 322 | " then extract "tag" from a:matchline and return "<tag>:</tag>" . |
| 323 | fun! s:InsertRefs(groupBR, prefix, group, suffix, matchline) |
| 324 | if a:matchline !~ a:prefix . |
| 325 | \ substitute(a:group, s:notslash . '\zs:', '\\|', 'g') . a:suffix |
| 326 | return a:group |
| 327 | endif |
| 328 | let i = matchend(a:groupBR, s:notslash . ':') |
| 329 | let ini = strpart(a:groupBR, 0, i-1) |
| 330 | let tailBR = strpart(a:groupBR, i) |
| 331 | let word = s:Choose(a:group, a:matchline, ":", "", a:prefix, a:suffix, |
| 332 | \ a:groupBR) |
| 333 | let i = matchend(word, s:notslash . ":") |
| 334 | let wordBR = strpart(word, i) |
| 335 | let word = strpart(word, 0, i-1) |
| 336 | " Now, a:matchline =~ a:prefix . word . a:suffix |
| 337 | if wordBR != ini |
| 338 | let table = s:Resolve(ini, wordBR, "table") |
| 339 | else |
| 340 | " let table = "----------" |
| 341 | let table = "" |
| 342 | let d = 0 |
| 343 | while d < 10 |
| 344 | if tailBR =~ s:notslash . '\\' . d |
| 345 | " let table[d] = d |
| 346 | let table = table . d |
| 347 | else |
| 348 | let table = table . "-" |
| 349 | endif |
| 350 | let d = d + 1 |
| 351 | endwhile |
| 352 | endif |
| 353 | let d = 9 |
| 354 | while d |
| 355 | if table[d] != "-" |
| 356 | let backref = substitute(a:matchline, a:prefix.word.a:suffix, |
| 357 | \ '\'.table[d], "") |
| 358 | " Are there any other characters that should be escaped? |
| 359 | let backref = escape(backref, '*,:') |
| 360 | execute s:Ref(ini, d, "start", "len") |
| 361 | let ini = strpart(ini, 0, start) . backref . strpart(ini, start+len) |
| 362 | let tailBR = substitute(tailBR, s:notslash . '\zs\\' . d, |
| 363 | \ escape(backref, '\\'), 'g') |
| 364 | endif |
| 365 | let d = d-1 |
| 366 | endwhile |
| 367 | if exists("b:match_debug") |
| 368 | if s:do_BR |
| 369 | let b:match_table = table |
| 370 | let b:match_word = word |
| 371 | else |
| 372 | let b:match_table = "" |
| 373 | let b:match_word = "" |
| 374 | endif |
| 375 | endif |
| 376 | return ini . ":" . tailBR |
| 377 | endfun |
| 378 | |
| 379 | " Input a comma-separated list of groups with backrefs, such as |
| 380 | " a:groups = '\(foo\):end\1,\(bar\):end\1' |
| 381 | " and return a comma-separated list of groups with backrefs replaced: |
| 382 | " return '\(foo\):end\(foo\),\(bar\):end\(bar\)' |
| 383 | fun! s:ParseWords(groups) |
| 384 | let groups = substitute(a:groups.",", s:notslash.'\zs[,:]*,[,:]*', ',', 'g') |
| 385 | let groups = substitute(groups, s:notslash . '\zs:\{2,}', ':', 'g') |
| 386 | let parsed = "" |
| 387 | while groups =~ '[^,:]' |
| 388 | let i = matchend(groups, s:notslash . ':') |
| 389 | let j = matchend(groups, s:notslash . ',') |
| 390 | let ini = strpart(groups, 0, i-1) |
| 391 | let tail = strpart(groups, i, j-i-1) . ":" |
| 392 | let groups = strpart(groups, j) |
| 393 | let parsed = parsed . ini |
| 394 | let i = matchend(tail, s:notslash . ':') |
| 395 | while i != -1 |
| 396 | " In 'if:else:endif', ini='if' and word='else' and then word='endif'. |
| 397 | let word = strpart(tail, 0, i-1) |
| 398 | let tail = strpart(tail, i) |
| 399 | let i = matchend(tail, s:notslash . ':') |
| 400 | let parsed = parsed . ":" . s:Resolve(ini, word, "word") |
| 401 | endwhile " Now, tail has been used up. |
| 402 | let parsed = parsed . "," |
| 403 | endwhile " groups =~ '[^,:]' |
| 404 | return parsed |
| 405 | endfun |
| 406 | |
| 407 | " TODO I think this can be simplified and/or made more efficient. |
| 408 | " TODO What should I do if a:start is out of range? |
| 409 | " Return a regexp that matches all of a:string, such that |
| 410 | " matchstr(a:string, regexp) represents the match for a:pat that starts |
| 411 | " as close to a:start as possible, before being preferred to after, and |
| 412 | " ends after a:start . |
| 413 | " Usage: |
| 414 | " let regexp = s:Wholematch(getline("."), 'foo\|bar', col(".")-1) |
| 415 | " let i = match(getline("."), regexp) |
| 416 | " let j = matchend(getline("."), regexp) |
| 417 | " let match = matchstr(getline("."), regexp) |
| 418 | fun! s:Wholematch(string, pat, start) |
| 419 | let group = '\%(' . a:pat . '\)' |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 420 | let prefix = (a:start ? '\(^.*\%<' . (a:start + 2) . 'c\)\zs' : '^') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 421 | let len = strlen(a:string) |
Bram Moolenaar | c81e5e7 | 2007-05-05 18:24:42 +0000 | [diff] [blame] | 422 | let suffix = (a:start+1 < len ? '\(\%>'.(a:start+1).'c.*$\)\@=' : '$') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 423 | if a:string !~ prefix . group . suffix |
| 424 | let prefix = '' |
| 425 | endif |
| 426 | return prefix . group . suffix |
| 427 | endfun |
| 428 | |
| 429 | " No extra arguments: s:Ref(string, d) will |
| 430 | " find the d'th occurrence of '\(' and return it, along with everything up |
| 431 | " to and including the matching '\)'. |
| 432 | " One argument: s:Ref(string, d, "start") returns the index of the start |
| 433 | " of the d'th '\(' and any other argument returns the length of the group. |
| 434 | " Two arguments: s:Ref(string, d, "foo", "bar") returns a string to be |
| 435 | " executed, having the effect of |
| 436 | " :let foo = s:Ref(string, d, "start") |
| 437 | " :let bar = s:Ref(string, d, "len") |
| 438 | fun! s:Ref(string, d, ...) |
| 439 | let len = strlen(a:string) |
| 440 | if a:d == 0 |
| 441 | let start = 0 |
| 442 | else |
| 443 | let cnt = a:d |
| 444 | let match = a:string |
| 445 | while cnt |
| 446 | let cnt = cnt - 1 |
| 447 | let index = matchend(match, s:notslash . '\\(') |
| 448 | if index == -1 |
| 449 | return "" |
| 450 | endif |
| 451 | let match = strpart(match, index) |
| 452 | endwhile |
| 453 | let start = len - strlen(match) |
| 454 | if a:0 == 1 && a:1 == "start" |
| 455 | return start - 2 |
| 456 | endif |
| 457 | let cnt = 1 |
| 458 | while cnt |
| 459 | let index = matchend(match, s:notslash . '\\(\|\\)') - 1 |
| 460 | if index == -2 |
| 461 | return "" |
| 462 | endif |
| 463 | " Increment if an open, decrement if a ')': |
| 464 | let cnt = cnt + (match[index]=="(" ? 1 : -1) " ')' |
| 465 | " let cnt = stridx('0(', match[index]) + cnt |
| 466 | let match = strpart(match, index+1) |
| 467 | endwhile |
| 468 | let start = start - 2 |
| 469 | let len = len - start - strlen(match) |
| 470 | endif |
| 471 | if a:0 == 1 |
| 472 | return len |
| 473 | elseif a:0 == 2 |
| 474 | return "let " . a:1 . "=" . start . "| let " . a:2 . "=" . len |
| 475 | else |
| 476 | return strpart(a:string, start, len) |
| 477 | endif |
| 478 | endfun |
| 479 | |
| 480 | " Count the number of disjoint copies of pattern in string. |
| 481 | " If the pattern is a literal string and contains no '0' or '1' characters |
| 482 | " then s:Count(string, pattern, '0', '1') should be faster than |
| 483 | " s:Count(string, pattern). |
| 484 | fun! s:Count(string, pattern, ...) |
| 485 | let pat = escape(a:pattern, '\\') |
| 486 | if a:0 > 1 |
| 487 | let foo = substitute(a:string, '[^'.a:pattern.']', "a:1", "g") |
| 488 | let foo = substitute(a:string, pat, a:2, "g") |
| 489 | let foo = substitute(foo, '[^' . a:2 . ']', "", "g") |
| 490 | return strlen(foo) |
| 491 | endif |
| 492 | let result = 0 |
| 493 | let foo = a:string |
| 494 | let index = matchend(foo, pat) |
| 495 | while index != -1 |
| 496 | let result = result + 1 |
| 497 | let foo = strpart(foo, index) |
| 498 | let index = matchend(foo, pat) |
| 499 | endwhile |
| 500 | return result |
| 501 | endfun |
| 502 | |
| 503 | " s:Resolve('\(a\)\(b\)', '\(c\)\2\1\1\2') should return table.word, where |
| 504 | " word = '\(c\)\(b\)\(a\)\3\2' and table = '-32-------'. That is, the first |
| 505 | " '\1' in target is replaced by '\(a\)' in word, table[1] = 3, and this |
| 506 | " indicates that all other instances of '\1' in target are to be replaced |
| 507 | " by '\3'. The hard part is dealing with nesting... |
| 508 | " Note that ":" is an illegal character for source and target, |
| 509 | " unless it is preceded by "\". |
| 510 | fun! s:Resolve(source, target, output) |
| 511 | let word = a:target |
| 512 | let i = matchend(word, s:notslash . '\\\d') - 1 |
| 513 | let table = "----------" |
| 514 | while i != -2 " There are back references to be replaced. |
| 515 | let d = word[i] |
| 516 | let backref = s:Ref(a:source, d) |
| 517 | " The idea is to replace '\d' with backref. Before we do this, |
| 518 | " replace any \(\) groups in backref with :1, :2, ... if they |
| 519 | " correspond to the first, second, ... group already inserted |
| 520 | " into backref. Later, replace :1 with \1 and so on. The group |
| 521 | " number w+b within backref corresponds to the group number |
| 522 | " s within a:source. |
| 523 | " w = number of '\(' in word before the current one |
| 524 | let w = s:Count( |
| 525 | \ substitute(strpart(word, 0, i-1), '\\\\', '', 'g'), '\(', '1') |
| 526 | let b = 1 " number of the current '\(' in backref |
| 527 | let s = d " number of the current '\(' in a:source |
| 528 | while b <= s:Count(substitute(backref, '\\\\', '', 'g'), '\(', '1') |
| 529 | \ && s < 10 |
| 530 | if table[s] == "-" |
| 531 | if w + b < 10 |
| 532 | " let table[s] = w + b |
| 533 | let table = strpart(table, 0, s) . (w+b) . strpart(table, s+1) |
| 534 | endif |
| 535 | let b = b + 1 |
| 536 | let s = s + 1 |
| 537 | else |
| 538 | execute s:Ref(backref, b, "start", "len") |
| 539 | let ref = strpart(backref, start, len) |
| 540 | let backref = strpart(backref, 0, start) . ":". table[s] |
| 541 | \ . strpart(backref, start+len) |
| 542 | let s = s + s:Count(substitute(ref, '\\\\', '', 'g'), '\(', '1') |
| 543 | endif |
| 544 | endwhile |
| 545 | let word = strpart(word, 0, i-1) . backref . strpart(word, i+1) |
| 546 | let i = matchend(word, s:notslash . '\\\d') - 1 |
| 547 | endwhile |
| 548 | let word = substitute(word, s:notslash . '\zs:', '\\', 'g') |
| 549 | if a:output == "table" |
| 550 | return table |
| 551 | elseif a:output == "word" |
| 552 | return word |
| 553 | else |
| 554 | return table . word |
| 555 | endif |
| 556 | endfun |
| 557 | |
| 558 | " Assume a:comma = ",". Then the format for a:patterns and a:1 is |
| 559 | " a:patterns = "<pat1>,<pat2>,..." |
| 560 | " a:1 = "<alt1>,<alt2>,..." |
| 561 | " If <patn> is the first pattern that matches a:string then return <patn> |
| 562 | " if no optional arguments are given; return <patn>,<altn> if a:1 is given. |
| 563 | fun! s:Choose(patterns, string, comma, branch, prefix, suffix, ...) |
| 564 | let tail = (a:patterns =~ a:comma."$" ? a:patterns : a:patterns . a:comma) |
| 565 | let i = matchend(tail, s:notslash . a:comma) |
| 566 | if a:0 |
| 567 | let alttail = (a:1 =~ a:comma."$" ? a:1 : a:1 . a:comma) |
| 568 | let j = matchend(alttail, s:notslash . a:comma) |
| 569 | endif |
| 570 | let current = strpart(tail, 0, i-1) |
| 571 | if a:branch == "" |
| 572 | let currpat = current |
| 573 | else |
Bram Moolenaar | 76b92b2 | 2006-03-24 22:46:53 +0000 | [diff] [blame] | 574 | let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 575 | endif |
| 576 | while a:string !~ a:prefix . currpat . a:suffix |
| 577 | let tail = strpart(tail, i) |
| 578 | let i = matchend(tail, s:notslash . a:comma) |
| 579 | if i == -1 |
| 580 | return -1 |
| 581 | endif |
| 582 | let current = strpart(tail, 0, i-1) |
| 583 | if a:branch == "" |
| 584 | let currpat = current |
| 585 | else |
Bram Moolenaar | 76b92b2 | 2006-03-24 22:46:53 +0000 | [diff] [blame] | 586 | let currpat = substitute(current, s:notslash . a:branch, '\\|', 'g') |
Bram Moolenaar | 071d427 | 2004-06-13 20:20:40 +0000 | [diff] [blame] | 587 | endif |
| 588 | if a:0 |
| 589 | let alttail = strpart(alttail, j) |
| 590 | let j = matchend(alttail, s:notslash . a:comma) |
| 591 | endif |
| 592 | endwhile |
| 593 | if a:0 |
| 594 | let current = current . a:comma . strpart(alttail, 0, j-1) |
| 595 | endif |
| 596 | return current |
| 597 | endfun |
| 598 | |
| 599 | " Call this function to turn on debugging information. Every time the main |
| 600 | " script is run, buffer variables will be saved. These can be used directly |
| 601 | " or viewed using the menu items below. |
| 602 | if !exists(":MatchDebug") |
| 603 | command! -nargs=0 MatchDebug call s:Match_debug() |
| 604 | endif |
| 605 | |
| 606 | fun! s:Match_debug() |
| 607 | let b:match_debug = 1 " Save debugging information. |
| 608 | " pat = all of b:match_words with backrefs parsed |
| 609 | amenu &Matchit.&pat :echo b:match_pat<CR> |
| 610 | " match = bit of text that is recognized as a match |
| 611 | amenu &Matchit.&match :echo b:match_match<CR> |
| 612 | " curcol = cursor column of the start of the matching text |
| 613 | amenu &Matchit.&curcol :echo b:match_col<CR> |
| 614 | " wholeBR = matching group, original version |
| 615 | amenu &Matchit.wh&oleBR :echo b:match_wholeBR<CR> |
| 616 | " iniBR = 'if' piece, original version |
| 617 | amenu &Matchit.ini&BR :echo b:match_iniBR<CR> |
| 618 | " ini = 'if' piece, with all backrefs resolved from match |
| 619 | amenu &Matchit.&ini :echo b:match_ini<CR> |
| 620 | " tail = 'else\|endif' piece, with all backrefs resolved from match |
| 621 | amenu &Matchit.&tail :echo b:match_tail<CR> |
| 622 | " fin = 'endif' piece, with all backrefs resolved from match |
| 623 | amenu &Matchit.&word :echo b:match_word<CR> |
| 624 | " '\'.d in ini refers to the same thing as '\'.table[d] in word. |
| 625 | amenu &Matchit.t&able :echo '0:' . b:match_table . ':9'<CR> |
| 626 | endfun |
| 627 | |
| 628 | " Jump to the nearest unmatched "(" or "if" or "<tag>" if a:spflag == "bW" |
| 629 | " or the nearest unmatched "</tag>" or "endif" or ")" if a:spflag == "W". |
| 630 | " Return a "mark" for the original position, so that |
| 631 | " let m = MultiMatch("bW", "n") ... execute m |
| 632 | " will return to the original position. If there is a problem, do not |
| 633 | " move the cursor and return "", unless a count is given, in which case |
| 634 | " go up or down as many levels as possible and again return "". |
| 635 | " TODO This relies on the same patterns as % matching. It might be a good |
| 636 | " idea to give it its own matching patterns. |
| 637 | fun! s:MultiMatch(spflag, mode) |
| 638 | if !exists("b:match_words") || b:match_words == "" |
| 639 | return "" |
| 640 | end |
| 641 | let restore_options = (&ic ? "" : "no") . "ignorecase" |
| 642 | if exists("b:match_ignorecase") |
| 643 | let &ignorecase = b:match_ignorecase |
| 644 | endif |
| 645 | let startline = line(".") |
| 646 | let startcol = col(".") |
| 647 | |
| 648 | " First step: if not already done, set the script variables |
| 649 | " s:do_BR flag for whether there are backrefs |
| 650 | " s:pat parsed version of b:match_words |
| 651 | " s:all regexp based on s:pat and the default groups |
| 652 | " This part is copied and slightly modified from s:Match_wrapper(). |
| 653 | let default = escape(&mps, '[$^.*~\\/?]') . (strlen(&mps) ? "," : "") . |
| 654 | \ '\/\*:\*\/,#if\%(def\)\=:$else\>:#elif\>:#endif\>' |
| 655 | " Allow b:match_words = "GetVimMatchWords()" . |
| 656 | if b:match_words =~ ":" |
| 657 | let match_words = b:match_words |
| 658 | else |
| 659 | execute "let match_words =" b:match_words |
| 660 | endif |
| 661 | if (match_words != s:last_words) || (&mps != s:last_mps) || |
| 662 | \ exists("b:match_debug") |
| 663 | let s:last_words = match_words |
| 664 | let s:last_mps = &mps |
| 665 | if match_words !~ s:notslash . '\\\d' |
| 666 | let s:do_BR = 0 |
| 667 | let s:pat = match_words |
| 668 | else |
| 669 | let s:do_BR = 1 |
| 670 | let s:pat = s:ParseWords(match_words) |
| 671 | endif |
| 672 | let s:all = '\%(' . substitute(s:pat . (strlen(s:pat)?",":"") . default, |
| 673 | \ '[,:]\+','\\|','g') . '\)' |
| 674 | if exists("b:match_debug") |
| 675 | let b:match_pat = s:pat |
| 676 | endif |
| 677 | endif |
| 678 | |
| 679 | " Second step: figure out the patterns for searchpair() |
| 680 | " and save the screen, cursor position, and 'ignorecase'. |
| 681 | " - TODO: A lot of this is copied from s:Match_wrapper(). |
| 682 | " - maybe even more functionality should be split off |
| 683 | " - into separate functions! |
| 684 | let cdefault = (s:pat =~ '[^,]$' ? "," : "") . default |
| 685 | let open = substitute(s:pat . cdefault, ':[^,]*,', '\\),\\(', 'g') |
| 686 | let open = '\(' . substitute(open, ':[^,]*$', '\\)', '') |
| 687 | let close = substitute(s:pat . cdefault, ',[^,]*:', '\\),\\(', 'g') |
| 688 | let close = substitute(close, '[^,]*:', '\\(', '') . '\)' |
| 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 |
| 731 | endfun |
| 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 |
| 792 | fun! 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 |
| 808 | endfun |
| 809 | |
| 810 | let &cpo = s:save_cpo |
| 811 | |
| 812 | " vim:sts=2:sw=2: |