├── LICENSE ├── README.md ├── autoload └── easyreplace.vim └── plugin └── easyreplace.vim /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 sosmo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-easyreplace 2 | 3 | Replace the substitute mode launched with the `c` flag (as in `:s/foo/bar/c`) with a solution that allows you to stay in normal mode and freely move / execute other commands while substituting. 4 | 5 | ![Sample pic](/../screenshots/1.gif?raw=true "easyreplace in action") 6 | 7 | 8 | ## Usage 9 | 10 | Type `:s/foo/bar` like you normally would to substitute "foo" with "bar". Then press the initiate key set by this plugin (`ctrl-enter` if you run Vim with a GUI, otherwise `ctrl-g` for the command line and `ctrl-b` for the command window). You'll be put back to normal mode on the next match of `foo`. Now you can press `ctrl-n` in normal mode replace the closest `foo` with `bar` (and move to the next `foo` if possible). Or, unlike with `:s/foo/bar/c`, you can do any normal mode stuff you feel like doing. Use `n` to skip results between `c-n`s as you normally would. 11 | 12 | You can prefix the `ctrl-n` command with a count to replace the next *n* matches at once. 13 | 14 | Press `cpr` (mnemonic: change previous replace) in normal mode to make easyreplace always replace the latest search result with your latest substitution. This is also the default if you haven't started a replace with the initiate key yet, which means you can just type `:s/foo/bar` and start replacing foos with bars by hitting `ctrl-n`. 15 | 16 | 17 | NOTE: If you are sure you'll never have multiple matches on one line, this built-in solution is probably better for you: 18 | 19 | nnoremap :&&j0gn`< 20 | 21 | The above replaces all the matches on the current line with the result of your latest substitution. The difference to easyreplace is that easyreplace can handle multiple matches on a line one by one, and is immune to new searches and substitutions if you want it to be. 22 | 23 | 24 | ## Installation 25 | 26 | Use your favorite plugin manager or just paste the files in your vim folder 27 | 28 | 29 | ## Configuration 30 | 31 | `:let g:erepl_after_initiate = "zz"` (default '') Replace the `zz` to issue any normal mode commands to executue after you've entered a regex. NOTE: special characters (such as ``) and `"` must be escaped with `\` 32 | 33 | `:let g:erepl_after_replace = "zz"` (default '') Same as g:erepl_after_initiate, except this gets executed after each substitution 34 | 35 | `:let g:erepl_always_verymagic = 1` (default 0) Treat the target regex when executing a new substitution as if it started with `\v` if no other modifiers are given 36 | 37 | **Available mappings:** 38 | 39 | `EasyReplaceInitiate` Map this to what you want to press in command line or cmdwindow to start the substitution. 40 | 41 | example: `:cmap EasyReplaceInitiate` 42 | 43 | `EasyReplaceDo` Map this to what you want to press in normal mode to substitute the current match and move to the next match after that. 44 | 45 | example: `:nmap EasyReplaceDo` 46 | 47 | `EasyReplaceInPlace` Map this to what you want to press in normal mode to substitute the current match and stay in place. 48 | 49 | example: `:nmap n EasyReplaceInPlace` 50 | 51 | `EasyReplaceBackwards` Map this to what you want to press in normal mode to substitute the current match and move to the preceding match. 52 | 53 | example: `:nmap EasyReplaceBackwards` 54 | 55 | `EasyReplaceToggleUsePrevious` Make easyreplace always replace the latest search result with your latest substitution. 56 | 57 | example: `:nmap cpr EasyReplaceToggleUsePrevious` 58 | 59 | `EasyReplaceArea` Map this in visual mode to replace all your selected matches at once. 60 | 61 | example: `:xmap EasyReplaceArea` 62 | 63 | 64 | ## Requirements 65 | 66 | * The 'history' setting set to 1 or more (sorry!) 67 | * Vim 7.4+ 68 | 69 | 70 | ## Bugs 71 | 72 | * Really long / complex searches might not work. The gn function used by this plugin tends to sometimes select only the first char on those, and these situations are where the plugin also fails. 73 | * Searches containing only one char might not work. gn sometimes fails to keep the cursor still when the match is only 1 char long and you're on it. gn might even skip these matches altogether. 74 | * If your substitution contains lookbehinds you might unwantedly get new matches or lose old ones between substitutions. This happens when the text seen by the next lookbehind changes after a substitution. If this ever becomes a problem you'll just have to use a normal substitution with the `c` flag instead. 75 | * Doesn't "bump up" search/cmd history items (if needed) after replaces, only after initializations. 76 | 77 | 78 | ## License 79 | 80 | Published under the MIT License. 81 | -------------------------------------------------------------------------------- /autoload/easyreplace.vim: -------------------------------------------------------------------------------- 1 | " Whether to use the latest search and latest substitution replacement (~) to find and replace when executing EasyReplaceDo 2 | let s:use_prev = 1 3 | " The string to be replaced with replace_str, also used for finding matches 4 | let s:match_str = '' 5 | " String to replace match_str 6 | let s:replace_str = '' 7 | " Flags of the substitution 8 | let s:flags = '' 9 | " The whole search is enclosed in one set of parentheses, and depending of the magic type of the search we either need to escape the latter paren or not 10 | let s:end_paren = '\)' 11 | " The position where the cursor left off after the last substitution operation 12 | let s:next_pos = [0, 0, 0, 0] 13 | 14 | 15 | " returns the index of the separator used in a subsitution command 16 | fun! easyreplace#FindSeparator(subst_cmd) 17 | let separator_i = -1 18 | let cont = 1 19 | " note that multi-byte chars throw this off, doesn't matter here because the result is used as a byte-wise index 20 | let len = strlen(a:subst_cmd) 21 | let i = 0 22 | while i < len && cont 23 | if strpart(a:subst_cmd, i, 1) ==# 's' 24 | let separator_i = match(a:subst_cmd, '\C\v((substitute|substitut|substitu|substit|substi|subst|subs|sub|su|s) *)@<=[^0-9A-Za-z_ ]', i) 25 | let cont = 0 26 | elseif strpart(a:subst_cmd, i, 1) ==# "'" 27 | let i += 2 28 | elseif strpart(a:subst_cmd, i, 1) ==# '/' 29 | let i = match(a:subst_cmd, '\v\\@()[]{}^$~|\') 54 | 55 | let substrings = split(strpart(a:cmd, separator_i+1), '\v\\@ 0 70 | call add(parts, '') 71 | let empty_places -= 1 72 | endwhile 73 | 74 | if len > 3 75 | let parts[3] = substitute(parts[3], '\v^ +| +$', '', 'g') 76 | endif 77 | 78 | return parts 79 | endfun 80 | 81 | 82 | fun! easyreplace#MatchBackwards(str, match) 83 | " note that multi-byte chars throw this off, doesn't matter here because the result is used as a byte-wise index 84 | let i = 0 85 | let found = -1 86 | while i >= 0 87 | let i = match(a:str, a:match, i) 88 | if i >= 0 89 | let found = i 90 | let i += 1 91 | endif 92 | endwhile 93 | return found 94 | endf 95 | 96 | 97 | fun! easyreplace#IsVeryMagic(search) 98 | let magic = "" 99 | " get the last magic operator 100 | let magic_i = easyreplace#MatchBackwards(a:search, '\C\v\\@= 0 102 | let magic = strpart(a:search, magic_i, 2) 103 | endif 104 | "" slow regex 105 | "let magic = matchstr(a:search, '\v\C(\\@= 0 118 | let i = match(a:str, a:match, i) 119 | if i >= 0 120 | let found += 1 121 | let i += 1 122 | endif 123 | endwhile 124 | return found 125 | endfun 126 | 127 | 128 | fun! easyreplace#CountChars(start_line, end_line) 129 | let line = a:start_line 130 | let chars = 0 131 | while line <= a:end_line 132 | let chars += strlen(substitute(getline(line), ".", "x", "g")) 133 | " also count newlines 134 | let chars += 1 135 | let line += 1 136 | endwhile 137 | return chars 138 | endfun 139 | 140 | 141 | fun! s:HandleFlags(flags) 142 | let s:flags = a:flags 143 | 144 | " remove possible "c" flag 145 | let s:flags = substitute(s:flags, '\Cc', '', "g") 146 | 147 | " conform to possible "I"/"i" flag 148 | let big_i = match(s:flags, '\CI') 149 | if big_i > -1 150 | let s:match_str .= "\\C" 151 | endif 152 | if match(s:flags, '\Ci') > big_i 153 | let s:match_str .= "\\c" 154 | endif 155 | 156 | " I don't think there's any reason to keep the 'n' flag 157 | let s:flags = substitute(s:flags, '\Cn', '', "g") 158 | endfun 159 | 160 | 161 | fun! s:HandleZsZe(search) 162 | let parts = split(a:search, '\C\v\\@ 1 166 | let zs = parts[0] 167 | let esc = '\' 168 | if easyreplace#IsVeryMagic(zs) 169 | let esc = '' 170 | endif 171 | let zs = '\%(' . zs . esc . ')' . esc . '@<=' 172 | let match_index = 1 173 | endif 174 | let parts = split(a:search, '\C\v\\@ 1 177 | let ze = parts[1] 178 | let start_esc = '\' 179 | if easyreplace#IsVeryMagic(parts[0]) 180 | let start_esc = '' 181 | endif 182 | let end_esc = '\' 183 | if easyreplace#IsVeryMagic(a:search) 184 | let end_esc = '' 185 | endif 186 | let ze = start_esc . '%(' . ze . end_esc . ')' . end_esc . '@=' 187 | endif 188 | let parts = split(a:search, '\C\v\\@ if one was found 216 | " strict: if 1 the first character of the match isn't allowed to be in front of the cursor, if 0 the match may start before the cursor as long as it's partially under it 217 | " end_paren can either be ')' if the match pattern uses verymagic or '\)' otherwise 218 | fun! s:FindNext(match, strict, wrap, end_paren) 219 | " set virtualedit onemore so you can move on linefeeds 220 | let user_virtualedit = &virtualedit 221 | set virtualedit=onemore 222 | 223 | let user_sel = &selection 224 | set sel=inclusive 225 | 226 | " if the search finds no matches vim will toggle wrapscan off for some reason so storing and restoring is easier than surrounding everything with try 227 | let user_wrapscan = &wrapscan 228 | 229 | let user_lazy = &lazyredraw 230 | set lazyredraw 231 | 232 | let start = getpos('.') 233 | let ret = 0 234 | 235 | " if the search wasn't strict, we can match any hit that the cursor is on (not just those that start at the cursor), and even hits behind the cursor if wrap is enabled. if we're on the first character of the buffer we don't have to worry about any of that 236 | " the delmarks approach seems to work here, not sure why 237 | if !a:strict || (line(".") == 1 && virtcol(".") == 1) 238 | delmarks <> 239 | 240 | let @/ = a:match 241 | set nowrapscan 242 | exe "normal! gn\" 243 | 244 | let found = line("'<") > 0 245 | if found 246 | let ret = 1 247 | elseif a:wrap 248 | set wrapscan 249 | keepj exe "sil! norm! ngn\" 250 | let found = line("'<") > 0 251 | if found 252 | let ret = 2 253 | endif 254 | endif 255 | if !found 256 | " make sure the marks exist 257 | norm! m 258 | endif 259 | " fix "s/aa/a" for "aaaa" and "s/x../xz" for "xyyxyy" by disallowing matches whose first character is behind the cursor 260 | else 261 | " when there are overlapping matches we need to limit the area of search to give priority to the match that starts at or after the cursor. otherwise the overlapping match gets skipped 262 | " note you can't use delmarks here. for some reason vim refuses to set the marks when you execute gn\ within the script (extra gv doesn't help either), so even if a match is found and gn selects the area, it's still impossible to know there was a match because the marks aren't set 263 | " note that \%'< or \%# don't seem to work as reliably, better stick with \%V 264 | call setpos("'<", start) 265 | call setpos("'>", [0, line("$"), col([line('$'), '$']), 0]) 266 | 267 | " without selecting the marks in visual mode you get false "not found" errors, so better keep it even if it seemingly can work without 268 | let temp = winsaveview() 269 | exe "norm! gv\" 270 | call winrestview(temp) 271 | 272 | " this is a dummy search and it's here because vim refuses to add a jump point with m' or setpos. this leaves a mark even with the 'keepj' 273 | keepj exe "silent! normal! h/\\%V\\%(" . a:match . a:end_paren . "\\" 274 | " histdel doesn't remove user's previous searches here, no need to worry about that 275 | call histdel("/", -1) 276 | call winrestview(temp) 277 | 278 | " set this so 'gn' works later on 279 | let @/ = '\%V\%(' . a:match . a:end_paren 280 | 281 | " note: search() fails when the match starts with a newline and cursor is directly next to it 282 | " this should solve that 283 | norm! h 284 | let found = search('\%V\%(' . a:match . a:end_paren, 'cW') 285 | 286 | if found != 0 287 | let ret = 1 288 | " if no match found but wrap is used 289 | elseif a:wrap 290 | let found = search(a:match, 'w') 291 | if found != 0 292 | let ret = 2 293 | endif 294 | endif 295 | " mark the first char of the match. after wrapping around this is mandatory because otherwise the result is outside of the visual marks 296 | " otherwise if no match was found, just make sure the marks exist 297 | norm! m 298 | if found != 0 299 | " mark the full match 300 | " gn sometimes fails to keep the cursor still when the match is only 1 char long and you're on it. most notably when the match is 1 char long and on the last col of a line. sometimes gn only selects the first char when the search string is complex. this may break the whole substitution at worst 301 | exe "normal! gn\" 302 | else 303 | " undo the previous 'h' if we didn't move to a new match 304 | call winrestview(temp) 305 | norm! m 306 | endif 307 | 308 | endif 309 | 310 | let &virtualedit = user_virtualedit 311 | let &selection = user_sel 312 | let &wrapscan = user_wrapscan 313 | let &lazyredraw = user_lazy 314 | 315 | let @/ = a:match 316 | 317 | keepj norm! `< 318 | return ret 319 | endfun 320 | 321 | 322 | fun! s:FindPrev(match, wrap) 323 | let user_virtualedit = &virtualedit 324 | set virtualedit=onemore 325 | 326 | let user_sel = &selection 327 | set sel=inclusive 328 | 329 | let user_wrapscan = &wrapscan 330 | 331 | let user_lazy = &lazyredraw 332 | set lazyredraw 333 | 334 | let ret = 0 335 | 336 | " dummy search to leave a mark 337 | let temp = winsaveview() 338 | normal! l 339 | keepj exe "silent! normal! ?" . a:match . "\\" 340 | call histdel("/", -1) 341 | call winrestview(temp) 342 | 343 | let @/ = a:match 344 | 345 | let found = search(a:match, 'bcW') 346 | 347 | if found != 0 348 | let ret = 1 349 | elseif a:wrap 350 | let found = search(a:match, 'bw') 351 | if found != 0 352 | let ret = 2 353 | endif 354 | endif 355 | norm! m 356 | if found != 0 357 | exe "normal! gn\" 358 | endif 359 | 360 | let &virtualedit = user_virtualedit 361 | let &selection = user_sel 362 | let &wrapscan = user_wrapscan 363 | let &lazyredraw = user_lazy 364 | 365 | keepj norm! `< 366 | return ret 367 | endfun 368 | 369 | 370 | fun! s:GetMsgLen() 371 | return float2nr(winwidth(0) / 1.5) - 17 372 | endfun 373 | 374 | 375 | fun! easyreplace#EasyReplaceToggleUsePrevious() 376 | let s:use_prev = !s:use_prev 377 | echo s:use_prev ? 'Mode: Use latest search' : 'Mode: Ignore searches' 378 | endfun 379 | 380 | 381 | fun! easyreplace#EasyReplaceInitiate(init_cmd) 382 | " make sure the marks exist, they're used later 383 | if line("'<") < 1 384 | call setpos("'<", '.') 385 | call setpos("'>", '.') 386 | endif 387 | 388 | " stop using latest substitution/search commands if explicitly initiated 389 | let s:use_prev = 0 390 | 391 | let parts = easyreplace#ParseSubstitution(a:init_cmd) 392 | if len(parts) <= 0 393 | echo 'easyreplace: Type a proper substitution command before initiating.' 394 | return 395 | endif 396 | 397 | let s:match_str = parts[1] 398 | if s:match_str ==# "" 399 | let s:match_str = @/ 400 | elseif g:erepl_always_verymagic && match(s:match_str, '\C\v^\\(v|m|V|M)') < 0 401 | let s:match_str = '\v' . s:match_str 402 | endif 403 | let s:match_str = s:HandleZsZe(s:match_str) 404 | 405 | let s:replace_str = parts[2] 406 | 407 | call s:HandleFlags(parts[3]) 408 | 409 | let s:end_paren = '\)' 410 | " choose the later parenthesis (defined by the presence of \v) to surround all the search string. this way \%V will always be applied to the whole search string, even with alternations present. it also makes ^ and such work in search 411 | if easyreplace#IsVeryMagic(s:match_str) 412 | let s:end_paren = ")" 413 | endif 414 | 415 | " if we use the previous search (leave the match part empty) and it was initiated from a substitution with a custom delimiter, vim will put the slashes non-escaped into the search register. however in order to use the previous search in the substitution all slashes that are NOT already escaped need to be escaped 416 | " same thing if the current substitution is written using a custom delimiter, all non-escaped slashes must be escaped 417 | " so we can safely escape non-escaped slashes in any situation 418 | " custom delimiters leave unnecessary escapes in the match, but vim luckily ignores them as long as their escaped forms aren't special characters in the current regex mode (which they shouldn't be) 419 | " always using '/' as the delimiter is smart because that way there won't be problems differentiating between escaped delimiters and escaped special characters 420 | let s:match_str = substitute(s:match_str, '\C\v\\@/".@/."\:call winrestview(g:erepl_prev_pos)\gn\`<:echo ''\", 'n') 431 | call feedkeys(g:erepl_after_initiate, 'n') 432 | endfunction 433 | 434 | 435 | " move to the next match after replacing if move is true 436 | " doesn't force highlight if it was disabled by the user after the initiation cmd, user can just press "n" to get it back 437 | fun! easyreplace#EasyReplaceDo(move) 438 | let original_left = getpos("'<") 439 | let original_right = getpos("'>") 440 | 441 | " wrapscan doesn't need to be stored, but if the search finds no matches vim will toggle wrapscan off for some reason so this is easier than surrounding everything with try 442 | let user_wrapscan = &wrapscan 443 | let user_virtualedit = &virtualedit 444 | let user_whichwrap = &whichwrap 445 | 446 | let user_sel = &selection 447 | set sel=inclusive 448 | 449 | let user_lazy = &lazyredraw 450 | set lazyredraw 451 | 452 | let msg_len = s:GetMsgLen() 453 | 454 | if s:use_prev 455 | call s:InitPrevSearch() 456 | else 457 | if s:match_str ==# "" 458 | return 459 | endif 460 | let @/ = s:match_str 461 | endif 462 | 463 | set whichwrap+=l 464 | set virtualedit=onemore 465 | 466 | let cycles = 0 467 | let times = v:count1 468 | while cycles < times 469 | " if the cursor is where the previous substitution left it, operate strictly. trying to emulate CursorMoved autocmd, obviously works differently when moving around and returning back, but that might not be bad at all 470 | " there's still a bug with lookbehinds. when you do a substitution it can change the results of the lookbehinds ahead of the substitution you just did. this can add new matches or remove existing matches unwantedly between substitutions. there's absolutely no way around this: if you store the next match before the substitution takes place, and remove lookbehinds from the substitution, you mess up backreferences and stuff. if you do a copy of the original buffer to locate matches, you can't edit anything so you might as well use the 'c' flag with a normal substitution instead. 471 | let found = s:FindNext(s:match_str, getpos(".") == s:next_pos, user_wrapscan, s:end_paren) 472 | " better disallow wrapping when using counts 473 | if found != 1 474 | break 475 | endif 476 | 477 | let user_reg = getreg('"') 478 | let user_reg_type = getregtype('"') 479 | " select the match marked by FindNext and yank it 480 | exe "normal! gv\"\"y\" 481 | 482 | let match = @" 483 | call setreg('"', user_reg, user_reg_type) 484 | 485 | " mark the first char of the next result so that the whole result (and not others, so no need to worry about the 'g' flag) is affected by \%V 486 | normal! m 487 | let original_line = line(".") 488 | let original_col = virtcol(".") 489 | 490 | " NOTE: tried using the built-in line2byte function to calculate substitution offset based on the total buffer byte count. ran into problems with some searches containing newlines at the start/end. 491 | 492 | " newlines should also count for length because virtualedit is enabled. '.' works in the pattern too 493 | let match_len = strlen(substitute(match, '\_.', 'x', 'g')) 494 | let match_height = easyreplace#SubStrCount(match, '\n') + 1 495 | 496 | let end_line = original_line + match_height - 1 497 | let chars_before = easyreplace#CountChars(original_line, end_line) 498 | 499 | exe "keepj '<,'>s/\\%V\\%(" . s:match_str . s:end_paren . '/' . s:replace_str . '/' . s:flags . 'e' 500 | 501 | let chars_after = easyreplace#CountChars(original_line, line(".")) 502 | let offset = match_len + (chars_after - chars_before) 503 | keepj exe 'normal! `<' 504 | if offset > 0 505 | exe 'normal! ' . offset . 'l' 506 | endif 507 | 508 | call histdel("/", -1) 509 | 510 | " while looping let next_pos not be on the next match to save a call to FindNext 511 | let s:next_pos = getpos(".") 512 | 513 | let cycles += 1 514 | 515 | endwhile 516 | 517 | if a:move && found == 1 518 | " if we found something without wrapping we replaced it and want to move to the next match 519 | let found = s:FindNext(s:match_str, 1, user_wrapscan, s:end_paren) 520 | if found == 1 521 | let s:next_pos = getpos(".") 522 | else 523 | let s:next_pos = [0, 0, 0, 0] 524 | endif 525 | else 526 | " we don't require strict searching if we wrapped around or found no matches or didn't move to the next match 527 | let s:next_pos = [0, 0, 0, 0] 528 | endif 529 | 530 | " these should technically go to feedkeys 531 | let msg = strpart(s:match_str, 0, msg_len) 532 | if found == 1 533 | echo '/' . msg 534 | elseif found == 2 535 | echohl WarningMsg 536 | echo 'Wrapped around: ' . msg 537 | echohl None 538 | else 539 | echohl WarningMsg 540 | echo 'No more matches: ' . msg 541 | echohl None 542 | endif 543 | 544 | let &virtualedit = user_virtualedit 545 | 546 | let &wrapscan = user_wrapscan 547 | let &whichwrap = user_whichwrap 548 | let &selection = user_sel 549 | let &lazyredraw = user_lazy 550 | 551 | call setpos("'<", original_left) 552 | call setpos("'>", original_right) 553 | 554 | call feedkeys(g:erepl_after_replace, 'n') 555 | endfunction 556 | 557 | 558 | " doesn't have a 'strict' option yet. it's not that useful and only would take effect after a forward replace 559 | fun! easyreplace#EasyReplaceDoBackwards() 560 | let original_left = getpos("'<") 561 | let original_right = getpos("'>") 562 | 563 | let user_wrapscan = &wrapscan 564 | let user_virtualedit = &virtualedit 565 | let user_whichwrap = &whichwrap 566 | 567 | let user_sel = &selection 568 | set sel=inclusive 569 | 570 | let user_lazy = &lazyredraw 571 | set lazyredraw 572 | 573 | let msg_len = s:GetMsgLen() 574 | 575 | if s:use_prev 576 | call s:InitPrevSearch() 577 | else 578 | if s:match_str ==# "" 579 | return 580 | endif 581 | let @/ = s:match_str 582 | endif 583 | 584 | set whichwrap+=l 585 | set virtualedit=onemore 586 | 587 | let cycles = 0 588 | let times = v:count1 589 | while cycles < times 590 | let found = s:FindPrev(s:match_str, user_wrapscan) 591 | let next_match = getpos('.') 592 | if found != 1 593 | let s:next_pos = [0, 0, 0, 0] 594 | break 595 | endif 596 | 597 | let view = winsaveview() 598 | 599 | norm! h 600 | let found = s:FindPrev(s:match_str, user_wrapscan) 601 | if found != 0 602 | let next_match = getpos('.') 603 | endif 604 | if found == 1 605 | let s:next_pos = next_match 606 | else 607 | let s:next_pos = [0, 0, 0, 0] 608 | endif 609 | 610 | call winrestview(view) 611 | normal! m 612 | 613 | exe "keepj '<,'>s/\\%V\\%(" . s:match_str . s:end_paren . '/' . s:replace_str . '/' . s:flags . 'e' 614 | 615 | call histdel("/", -1) 616 | 617 | let cycles += 1 618 | endwhile 619 | 620 | call setpos('.', next_match) 621 | 622 | " if the second FindPrev wrapped around there's a possibility we replaced the only match it found 623 | if found == 2 624 | let retry = s:FindPrev(s:match_str, user_wrapscan) 625 | if retry == 0 626 | let found = 0 627 | endif 628 | endif 629 | 630 | let msg = strpart(s:match_str, 0, msg_len) 631 | if found == 1 632 | echo '?' . msg 633 | elseif found == 2 634 | echohl WarningMsg 635 | echo 'Wrapped around: ' . msg 636 | echohl None 637 | else 638 | echohl WarningMsg 639 | echo 'No more matches: ' . msg 640 | echohl None 641 | endif 642 | 643 | let &virtualedit = user_virtualedit 644 | 645 | let &wrapscan = user_wrapscan 646 | let &whichwrap = user_whichwrap 647 | let &selection = user_sel 648 | let &lazyredraw = user_lazy 649 | 650 | call setpos("'<", original_left) 651 | call setpos("'>", original_right) 652 | 653 | call feedkeys(g:erepl_after_replace, 'n') 654 | endfunction 655 | 656 | 657 | fun! easyreplace#SubstituteArea() 658 | if s:use_prev 659 | call s:InitPrevSearch() 660 | else 661 | if s:match_str ==# "" 662 | return 663 | endif 664 | let @/ = s:match_str 665 | endif 666 | 667 | let search = @/ 668 | 669 | exe "silent! keepj '<,'>s/\\%V\\%(" . s:match_str . s:end_paren . '/' . s:replace_str . '/' . s:flags . 'e' 670 | 671 | call histdel("/", -1) 672 | let @/ = search 673 | 674 | redraw 675 | let msg_len = s:GetMsgLen() 676 | let msg = strpart(s:match_str, 0, msg_len) 677 | echo msg 678 | endfun 679 | -------------------------------------------------------------------------------- /plugin/easyreplace.vim: -------------------------------------------------------------------------------- 1 | "# vim-easyreplace 2 | " 3 | "Replace the substitute mode launched with the `c` flag (as in `:s/foo/bar/c`) with a solution that allows you to stay in normal mode and freely move / execute other commands while substituting. 4 | " 5 | "![Sample pic](/../screenshots/1.gif?raw=true "easyreplace in action") 6 | " 7 | " 8 | "## Usage 9 | " 10 | "Type `:s/foo/bar` like you normally would to substitute "foo" with "bar". Then press the initiate key set by this plugin (`ctrl-enter` if you run Vim with a GUI, otherwise `ctrl-g` for the command line and `ctrl-b` for the command window). You'll be put back to normal mode on the next match of `foo`. Now you can press `ctrl-n` in normal mode replace the closest `foo` with `bar` (and move to the next `foo` if possible). Or, unlike with `:s/foo/bar/c`, you can do any normal mode stuff you feel like doing. Use `n` to skip results between `c-n`s as you normally would. 11 | " 12 | "You can prefix the `ctrl-n` command with a count to replace the next *n* matches at once. 13 | " 14 | "Press `cpr` (mnemonic: change previous replace) in normal mode to make easyreplace always replace the latest search result with your latest substitution. This is also the default if you haven't started a replace with the initiate key yet, which means you can just type `:s/foo/bar` and start replacing foos with bars by hitting `ctrl-n`. 15 | " 16 | " 17 | "NOTE: If you are sure you'll never have multiple matches on one line, this built-in solution is probably better for you: 18 | " 19 | " nnoremap :&&j0gn`< 20 | " 21 | "The above replaces all the matches on the current line with the result of your latest substitution. The difference to easyreplace is that easyreplace can handle multiple matches on a line one by one, and is immune to new searches and substitutions if you want it to be. 22 | " 23 | " 24 | "## Installation 25 | " 26 | "Use your favorite plugin manager or just paste the files in your vim folder 27 | " 28 | " 29 | "## Configuration 30 | " 31 | "`:let g:erepl_after_initiate = "zz"` (default '') Replace the `zz` to issue any normal mode commands to executue after you've entered a regex. NOTE: special characters (such as ``) and `"` must be escaped with `\` 32 | " 33 | "`:let g:erepl_after_replace = "zz"` (default '') Same as g:erepl_after_initiate, except this gets executed after each substitution 34 | " 35 | "`:let g:erepl_always_verymagic = 1` (default 0) Treat the target regex when executing a new substitution as if it started with `\v` if no other modifiers are given 36 | " 37 | "**Available mappings:** 38 | " 39 | "`EasyReplaceInitiate` Map this to what you want to press in command line or cmdwindow to start the substitution. 40 | " 41 | "example: `:cmap EasyReplaceInitiate` 42 | " 43 | "`EasyReplaceDo` Map this to what you want to press in normal mode to substitute the current match and move to the next match after that. 44 | " 45 | "example: `:nmap EasyReplaceDo` 46 | " 47 | "`EasyReplaceInPlace` Map this to what you want to press in normal mode to substitute the current match and stay in place. 48 | " 49 | "example: `:nmap n EasyReplaceInPlace` 50 | " 51 | "`EasyReplaceBackwards` Map this to what you want to press in normal mode to substitute the current match and move to the preceding match. 52 | " 53 | "example: `:nmap EasyReplaceBackwards` 54 | " 55 | "`EasyReplaceToggleUsePrevious` Make easyreplace always replace the latest search result with your latest substitution. 56 | " 57 | "example: `:nmap cpr EasyReplaceToggleUsePrevious` 58 | " 59 | "`EasyReplaceArea` Map this in visual mode to replace all your selected matches at once. 60 | " 61 | "example: `:xmap EasyReplaceArea` 62 | " 63 | " 64 | "## Requirements 65 | " 66 | "* The 'history' setting set to 1 or more (sorry!) 67 | "* Vim 7.4+ 68 | " 69 | " 70 | "## Bugs 71 | " 72 | "* Really long / complex searches might not work. The gn function used by this plugin tends to sometimes select only the first char on those, and these situations are where the plugin also fails. 73 | "* Searches containing only one char might not work. gn sometimes fails to keep the cursor still when the match is only 1 char long and you're on it. gn might even skip these matches altogether. 74 | "* If your substitution contains lookbehinds you might unwantedly get new matches or lose old ones between substitutions. This happens when the text seen by the next lookbehind changes after a substitution. If this ever becomes a problem you'll just have to use a normal substitution with the `c` flag instead. 75 | "* Doesn't "bump up" search/cmd history items (if needed) after replaces, only after initializations. 76 | " 77 | " 78 | "## License 79 | " 80 | "Published under the MIT License. 81 | 82 | 83 | if exists('g:erepl_loaded') 84 | finish 85 | endif 86 | let g:erepl_loaded = 1 87 | 88 | 89 | " define user custom operations to run after each easyreplace initiation or replace operation. special characters (such as "") and """ must be escaped with "\" 90 | if !exists("g:erepl_after_initiate") 91 | let g:erepl_after_initiate = "" 92 | endif 93 | if !exists("g:erepl_after_replace") 94 | let g:erepl_after_replace = "" 95 | endif 96 | if !exists("g:erepl_always_verymagic") 97 | let g:erepl_always_verymagic = 0 98 | endif 99 | 100 | 101 | " a bit ugly, but has to be mapped like this to escape cmd and record typed command to cmd history 102 | inoremap EasyReplaceInitiate :call easyreplace#EasyReplaceInitiate(histget(":", -1)) 103 | nnoremap EasyReplaceInitiate :call easyreplace#EasyReplaceInitiate(histget(":", -1)) 104 | cnoremap EasyReplaceInitiate :call easyreplace#EasyReplaceInitiate(histget(":", -1)) 105 | 106 | nnoremap EasyReplaceDo :call easyreplace#EasyReplaceDo(1) 107 | 108 | nnoremap EasyReplaceInPlace :call easyreplace#EasyReplaceDo(0) 109 | 110 | nnoremap EasyReplaceToggleUsePrevious :call easyreplace#EasyReplaceToggleUsePrevious() 111 | 112 | nnoremap EasyReplaceBackwards :call easyreplace#EasyReplaceDoBackwards() 113 | 114 | xnoremap EasyReplaceArea :call easyreplace#SubstituteArea() 115 | 116 | 117 | " doesn't work for most terminals. 118 | " for terminals by default use to initiate from insert mode (cmdwindow) and to initiate from cmdline 119 | if has("gui_running") 120 | if !hasmapto('EasyReplaceInitiate', 'i') && maparg('', 'i') ==# '' 121 | imap EasyReplaceInitiate 122 | endif 123 | if !hasmapto('EasyReplaceInitiate', 'n') && maparg('', 'n') ==# '' 124 | nmap EasyReplaceInitiate 125 | endif 126 | if !hasmapto('EasyReplaceInitiate', 'c') && maparg('', 'c') ==# '' 127 | cmap EasyReplaceInitiate 128 | endif 129 | else 130 | if !hasmapto('EasyReplaceInitiate', 'i') && maparg('', 'i') ==# '' 131 | imap EasyReplaceInitiate 132 | endif 133 | if !hasmapto('EasyReplaceInitiate', 'c') && maparg('', 'c') ==# '' 134 | cmap EasyReplaceInitiate 135 | endif 136 | endif 137 | 138 | if !hasmapto('EasyReplaceDo', 'n') && maparg('', 'n') ==# '' 139 | nmap EasyReplaceDo 140 | endif 141 | 142 | " mnemonic: change previous replace 143 | if !hasmapto('EasyReplaceToggleUsePrevious', 'n') && maparg('cpr', 'n') ==# '' 144 | nmap cpr EasyReplaceToggleUsePrevious 145 | endif 146 | --------------------------------------------------------------------------------