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