let s:cpo_save = &cpo set cpo&vim let s:timeout = get(g:, 'traces_timeout', 1000) let s:timeout = s:timeout > 200 ? s:timeout : 200 let s:search_timeout = get(g:, 'traces_search_timeout', 500) let s:search_timeout = s:search_timeout > s:timeout - 100 ? s:timeout - 100 : s:search_timeout let s:has_matchdelete_win = has('patch-8.1.1741') let s:cmd_pattern = '\v\C^%(' \ . 'g%[lobal][[:alnum:]]@!\!=|' \ . 's%[ubstitute][[:alnum:]]@!|' \ . '(Subvert|S)[[:alnum:]]@!|' \ . 'sm%[agic][[:alnum:]]@!|' \ . 'sno%[magic][[:alnum:]]@!|' \ . 'sor%[t][[:alnum:]]@!\!=|' \ . 'norm%[al][[:alnum:]]@!\!=|' \ . 'v%[global][[:alnum:]]@!' \ . ')' let s:buf = {} function! s:trim(...) abort if a:0 == 2 let a:1[0] = strpart(a:1[0], a:2) else let a:1[0] = substitute(a:1[0], '^\s\+', '', '') endif endfunction function! s:parse_range(range, cmdl) abort let specifier = {} let specifier.addresses = [] let while_limit = 0 while 1 " address part call s:trim(a:cmdl) let entry = {} " regexp for pattern specifier let pattern = '/%(\\.|/@!&.)*/=|\?%(\\.|\?@!&.)*\?=' if !len(specifier.addresses) let address = matchstrpos(a:cmdl[0], '\v^%(\d+|\.|\$|\%|\*|''.|'. pattern . '|\\\/|\\\?|\\\&)') else let address = matchstrpos(a:cmdl[0], '\v^%(' . pattern . ')' ) endif if address[2] != -1 call s:trim(a:cmdl, address[2]) let entry.str = address[0] endif " offset call s:trim(a:cmdl) let offset = matchstrpos(a:cmdl[0], '\m^\%(\d\|\s\|+\|-\)\+') if offset[2] != -1 call s:trim(a:cmdl, offset[2]) let entry.offset = offset[0] endif " add first address if address[2] != -1 || offset[2] != -1 " if offset is present but specifier is missing add '.' specifier if !has_key(entry, 'str') let entry.str = '.' endif call add(specifier.addresses, entry) else " stop trying if previous attempt was unsuccessful break endif let while_limit += 1 | if while_limit == 1000 \ | echoerr 'infinite loop' | break | endif endwhile " delimiter call s:trim(a:cmdl) let delimiter = matchstrpos(a:cmdl[0], '\m^\%(,\|;\)') if delimiter[2] != -1 call s:trim(a:cmdl, delimiter[2]) let specifier.delimiter = delimiter[0] endif " add when addresses or delimiter are found or when one specifier is " already known if !empty(specifier.addresses) || delimiter[2] != -1 || !empty(a:range) " specifiers are not given but delimiter is present if empty(specifier.addresses) call add(specifier.addresses, { 'str': '.' }) endif call add(a:range, specifier) endif if delimiter[2] != -1 try return s:parse_range(a:range, a:cmdl) catch /^Vim\%((\a\+)\)\=:E132/ return [] endtry else return a:range endif endfunction " five cases (+11 -11 11 -- ++) function! s:offset_to_num(string) abort let offset = 0 let copy = a:string let input = [copy] let pattern = '\m^\%(+\d\+\|-\d\+\|\d\+\|-\+\ze-\d\|+\+\ze+\d\|-\+\|+\+\)' let while_limit = 0 while !empty(input[0]) call s:trim(input) let part = matchstrpos(input[0], pattern) call s:trim(input, part[2]) if part[0] =~# '+\d\+' let offset += str2nr(matchstr(part[0], '\d\+')) elseif part[0] =~# '-\d\+' let offset -= str2nr(matchstr(part[0], '\d\+')) elseif part[0] =~# '\d\+' let offset += str2nr(part[0]) elseif part[0] =~# '+\+' let offset += strchars(part[0]) elseif part[0] =~# '-\+' let offset -= strchars(part[0]) endif let while_limit += 1 | if while_limit == 1000 \ | echoerr 'infinite loop' | break | endif endwhile return offset endfunction function! s:search(...) abort let cache = s:buf[s:nr].search_cache let key = string(a:000[0:2]) . string(getcurpos()) if has_key(cache, key) if a:0 is 1 || a:0 >= 2 && a:2 !~# 'n' call setpos('.', cache[key].curpos) endif return cache[key].lnum endif if s:search_timeout_remaining <= 0 return 0 endif let start = reltime() silent! let lnum = call('search', a:000) let time = reltimefloat(reltime(start)) * 1000 let s:search_timeout_remaining -= float2nr(ceil(time)) let cache[key] = {'lnum': lnum, 'curpos': getcurpos()} return lnum endfunction function! s:address_to_num(address, last_pos) abort let result = {} let result.range = [] let result.valid = 1 let result.regex = '' let s:entire_file = 0 if a:address.str =~# '^\d\+' let lnum = str2nr(a:address.str) call add(result.range, lnum) elseif a:address.str ==# '.' call add(result.range, a:last_pos) elseif a:address.str ==# '$' call add(result.range, getpos('$')[1]) elseif a:address.str ==# '%' call add(result.range, 1) call add(result.range, getpos('$')[1]) let s:entire_file = 1 elseif a:address.str ==# '*' call add(result.range, getpos('''<')[1]) call add(result.range, getpos('''>')[1]) if match(&cpoptions, '\*') != -1 let result.valid = 0 endif let s:buf[s:nr].show_range = 1 elseif a:address.str =~# '^''.' call cursor(a:last_pos, 1) let mark_position = getpos(a:address.str) if mark_position[1] call add(result.range, mark_position[1]) else let result.valid = 0 endif let s:buf[s:nr].show_range = 1 elseif a:address.str[0] is '/' let closed = a:address.str =~# '\v%([^\\]\\)@ line('$') || result.range[0] < 0 let result.valid = 0 endif endif return result endfunction function! s:evaluate_range(range_structure) abort let result = { 'range': [], 'pattern': '', 'end': ''} let s:range_valid = 1 let pos = s:buf[s:nr].cur_init_pos[0] for specifier in a:range_structure let tmp_pos = pos let specifier_result = [] for address in specifier.addresses " skip empty unclosed pattern specifier when range is empty otherwise " substitute it with current position if address.str =~# '^[?/]$' let s:buf[s:nr].show_range = 1 if empty(result.range) break endif let address.str = '.' endif let query = s:address_to_num(address, tmp_pos) " % specifier doesn't accept additional addresses if !query.valid || len(query.range) == 2 && len(specifier.addresses) > 1 let s:range_valid = 0 break endif let tmp_pos = query.range[-1] let specifier_result = deepcopy(query.range) let result.pattern = query.regex endfor if !s:range_valid break endif call extend(result.range, specifier_result) if exists('specifier.delimiter') let s:specifier_delimiter = 1 endif if get(specifier, 'delimiter') is# ';' let pos = result.range[-1] endif endfor if !empty(result.range) let result.end = result.range[-1] if len(result.range) == 1 call extend(result.range, result.range) else let result.range = result.range[-2:-1] if result.range[-1] < result.range[-2] let temp = result.range[-2] let result.range[-2] = result.range[-1] let result.range[-1] = temp endif endif endif return s:range_valid ? result : { 'range': [], 'pattern': '', 'end': '' } endfunction function! s:get_selection_regexp(range) abort if empty(a:range) return '' endif let pattern = '\%>' . (a:range[-2] - 1) . 'l\%<' . (a:range[-1] + 1) . 'l' if &listchars =~# 'eol:.' let pattern .= '\_.' else let pattern .= '\(.\|^\)' endif return pattern endfunction function! s:get_command(cmdl) abort call s:trim(a:cmdl) if !s:range_valid return '' endif let result = matchstrpos(a:cmdl[0], s:cmd_pattern) if result[2] != -1 call s:trim(a:cmdl, result[2]) return result[0] endif return '' endfunction function! s:add_opt(pattern, cmdl) abort if empty(a:pattern) || !s:range_valid \ || empty(substitute(a:pattern, '\\[cCvVmM]', '', 'g')) return '' endif let option = '' " magic if has_key(a:cmdl, 'cmd') && a:cmdl.cmd.name =~# '\v^sm%[agic]$' let option = '\m' elseif has_key(a:cmdl, 'cmd') && a:cmdl.cmd.name =~# '\v^sno%[magic]$' let option = '\M' elseif &magic let option = '\m' else let option = '\M' endif " case if &ignorecase if &smartcase if match(a:pattern, '\u') ==# -1 let option .= '\c' else let option .= '\C' endif else let option .= '\c' endif endif return option . a:pattern endfunction function! s:add_hl_guard(pattern, range, type) abort if empty(a:pattern) return '' endif if a:type is 's' if empty(a:range) let start = s:buf[s:nr].cur_init_pos[0] - 1 let end = s:buf[s:nr].cur_init_pos[0] + 1 else let start = a:range[-2] - 1 let end = a:range[-1] + 1 endif elseif a:type is 'g' if empty(a:range) return a:pattern else let start = a:range[-2] - 1 let end = a:range[-1] + 1 endif elseif a:type is 'r' let start = a:range - 1 let end = a:range + 1 endif let range = '\m\%>'. start .'l' . '\%<' . end . 'l' " group is necessary to contain pattern inside range when using branches (\|) let group_start = '\%(' let group_end = '\m\)' " add backslash to the end of pattern if it ends with odd number of " backslashes, this is required to properly close group if len(matchstr(a:pattern, '\\\+$')) % 2 let group_end = '\' . group_end endif return range . group_start . a:pattern . group_end endfunction function! s:parse_global(cmdl) abort call s:trim(a:cmdl.string) let pattern = '\v^([[:graph:]]&[^[:alnum:]\\"|])(%(\\.|.){-})%((\1)|$)' let args = {} let r = matchlist(a:cmdl.string[0], pattern) if len(r) let args.delimiter = r[1] let args.pattern = s:add_opt((empty(r[2]) && !empty(r[3])) ? s:last_pattern : r[2], a:cmdl) endif return args endfunction function! s:parse_substitute(cmdl) abort call s:trim(a:cmdl.string) let pattern = '\v^([[:graph:]]&[^[:alnum:]\\"|])(%(\\.|\1@!&.)*)%((\1)%((%(\\.|\1@!&.)*)%((\1)([&cegiInp#lr]+)=)=)=)=$' let args = {} let r = matchlist(a:cmdl.string[0], pattern) if len(r) let args.delimiter = r[1] let args.pattern_org = (empty(r[2]) && !empty(r[3])) ? s:last_pattern : r[2] let args.pattern = s:add_opt((empty(r[2]) && !empty(r[3])) ? s:last_pattern : r[2], a:cmdl) let args.string = r[4] let args.last_delimiter = r[5] let args.flags = r[6] endif return args endfunction function! s:parse_subvert(cmdl) abort if !exists("g:loaded_abolish") || !g:traces_abolish_integration return {} endif if !exists('s:abolishID') " https://stackoverflow.com/a/39216373 " 'dirty trick' to access script-local functions let s:abolishID = '' . matchstr(matchstr(split(execute('scriptnames'), "\n"), 'abolish.vim'), '^\s*\zs\d\+') . '_' endif if !exists('*' . s:abolishID . 'substitute_command') return {} endif call s:trim(a:cmdl.string) let a:cmdl.cmd.name = 'substitute' let pattern = '\v^([[:graph:]]&[^[:alnum:]\\"|])(%(\\.|\1@!&.)*)%((\1)%((%(\\.|\1@!&.)*)%((\1)([aviw&cegiInp#lr]+)=)=)=)=$' let args = {} let r = matchlist(a:cmdl.string[0], pattern) if len(r) && !empty(r[2]) " String is always '\=Abolished()', unlet to update preview window if exists('s:buf[s:nr].preview_window.args') unlet s:buf[s:nr].preview_window.args endif let args.delimiter = r[1] let args.pattern_org = substitute({s:abolishID}substitute_command('', r[2], r[4], r[6])[1:], '\/\\=Abolished.*', '', '') let args.pattern = args.pattern_org let args.string = !empty(r[4]) ? '\=Abolished()' : '' let args.last_delimiter = r[5] let args.flags = substitute(r[6], '\C[avIiw]', '', 'g') endif return args endfunction function! s:parse_sort(cmdl) abort call s:trim(a:cmdl.string) let pattern = '\v^.{-}([[:graph:]]&[^[:alnum:]\\"|])(%(\\.|.){-})%((\1)|$)' let args = {} let r = matchlist(a:cmdl.string[0], pattern) if len(r) let args.delimiter = r[1] let args.pattern = s:add_opt((empty(r[2]) && !empty(r[3])) ? s:last_pattern : r[2], a:cmdl) endif return args endfunction function! s:parse_normal(cmdl) abort let args = {} call s:trim(a:cmdl.string) let args.string = a:cmdl.string return args endfunction function! s:parse_command(cmdl) abort let a:cmdl.cmd.name = s:get_command(a:cmdl.string) if a:cmdl.cmd.name =~# '\v^%(g%[lobal]\!=|v%[global])$' let a:cmdl.cmd.args = s:parse_global(a:cmdl) elseif a:cmdl.cmd.name =~# '\v^%(s%[ubstitute]|sm%[agic]|sno%[magic])$' let a:cmdl.cmd.args = s:parse_substitute(a:cmdl) elseif a:cmdl.cmd.name =~# '\v^%(sor%[t]\!=)$' let a:cmdl.cmd.args = s:parse_sort(a:cmdl) elseif a:cmdl.cmd.name =~# '\v^norm%[al]\!=$' let a:cmdl.cmd.args = s:parse_normal(a:cmdl) elseif a:cmdl.cmd.name =~# '\v^(Subvert|S)$' let a:cmdl.cmd.args = s:parse_subvert(a:cmdl) endif endfunction function! s:pos_pattern(pattern, range, delimiter, type) abort if g:traces_preserve_view_state || empty(a:pattern) return endif let stopline = 0 if len(a:range) > 1 && !get(s:, 'entire_file') if a:delimiter ==# '?' call cursor([a:range[-1], 1]) call cursor([a:range[-1], col('$')]) let stopline = a:range[-2] else call cursor([a:range[-2], 1]) let stopline = a:range[-1] endif else call cursor(s:buf[s:nr].cur_init_pos) if a:type && empty(a:range) let stopline = s:buf[s:nr].cur_init_pos[0] endif endif if a:delimiter ==# '?' let position = s:search(a:pattern, 'cb', stopline, s:search_timeout_remaining) else let position = s:search(a:pattern, 'c', stopline, s:search_timeout_remaining) endif if position !=# 0 let s:moved = 1 endif endfunction function! s:pos_range(end, pattern) abort if g:traces_preserve_view_state || empty(a:end) return endif if exists('s:buf[s:nr].pre_cmdl_view') if get(s:buf[s:nr].pre_cmdl_view, 'mode', '') =~# "^[vV\]" \ && a:end > line('w$') unlet s:buf[s:nr].pre_cmdl_view.mode call winrestview(s:buf[s:nr].pre_cmdl_view) endif unlet s:buf[s:nr].pre_cmdl_view endif call cursor([a:end, 1]) if !empty(a:pattern) call s:search(a:pattern, 'c', a:end, s:search_timeout_remaining) endif let s:moved = 1 endfunction function! s:highlight(group, pattern, priority) abort let cur_win = s:buf[s:nr].cur_win if exists('s:buf[s:nr].win[cur_win].matches[a:group].pattern') if s:buf[s:nr].win[cur_win].matches[a:group].pattern ==# a:pattern return endif elseif empty(a:pattern) return endif if a:group ==# 'TracesSearch' || a:group ==# 'TracesReplace' noautocmd let &hlsearch = 0 endif noautocmd let &winwidth = max([1, &winminwidth]) noautocmd let &winheight = max([1, &winminheight]) let windows = filter(win_findbuf(s:nr), {_, val -> win_id2win(val)}) if empty(s:buf[s:nr].win) " save local options for id in windows let s:buf[s:nr].win[id] = {} let win = s:buf[s:nr].win[id] let win.options = {} let win.options.cursorcolumn = getwinvar(id, '&cursorcolumn') let win.options.cursorline = getwinvar(id, '&cursorline') let win.options.scrolloff = getwinvar(id, '&scrolloff') let win.options.conceallevel = getwinvar(id, '&conceallevel') let win.options.concealcursor = getwinvar(id, '&concealcursor') endfor " set local options for id in windows call setwinvar(id, '&' . 'cursorcolumn', 0) call setwinvar(id, '&' . 'cursorline', 0) if id isnot s:buf[s:nr].cur_win call setwinvar(id, '&' . 'scrolloff', 0) endif endfor endif " add matches for id in windows if getwininfo(id)[0].height is 0 || getwininfo(id)[0].width is 0 " skip minimized windows continue endif if empty(getcmdwintype()) && !s:has_matchdelete_win noautocmd call win_gotoid(id) endif let win = s:buf[s:nr].win[id] let win.matches = get(win, 'matches', {}) if !exists('win.matches[a:group]') if s:has_matchdelete_win silent! let match_id = matchadd(a:group, a:pattern, a:priority, -1, {'window': id}) else silent! let match_id = matchadd(a:group, a:pattern, a:priority) endif let win.matches[a:group] = {'match_id': match_id, 'pattern': a:pattern} let s:redraw_later = 1 if a:group ==# 'Conceal' call setwinvar(id, '&conceallevel', 2) call setwinvar(id, '&concealcursor', 'c') endif else if s:has_matchdelete_win silent! call matchdelete(win.matches[a:group].match_id, id) silent! let match_id = matchadd(a:group, a:pattern, a:priority, -1, {'window': id}) else silent! call matchdelete(win.matches[a:group].match_id) silent! let match_id = matchadd(a:group, a:pattern, a:priority) endif let win.matches[a:group] = {'match_id': match_id, 'pattern': a:pattern} let s:redraw_later = 1 endif endfor if empty(getcmdwintype()) && !s:has_matchdelete_win noautocmd call win_gotoid(s:buf[s:nr].cur_win) endif endfunction function! s:format_range(cmdl) abort let range_str = '' if len(a:cmdl.range.abs) == 0 let range_str .= s:buf[s:nr].cur_init_pos[0] elseif len(a:cmdl.range.abs) == 1 let range_str .= a:cmdl.range.abs[0] else let range_str .= max([a:cmdl.range.abs[-2], line("w0")]) let range_str .= ';' let range_str .= min([a:cmdl.range.abs[-1], line("w$")]) endif return range_str endfunction function! s:format_command(cmdl) abort let cmd_str = '' let cmd_str .= a:cmdl.cmd.name let cmd_str .= a:cmdl.cmd.args.delimiter let cmd_str .= a:cmdl.cmd.args.pattern_org let cmd_str .= a:cmdl.cmd.args.delimiter let s_mark = s:buf[s:nr].s_mark if a:cmdl.cmd.args.string =~# '^\\=' let cmd_str .= printf("\\='%s' . printf('%%s', %s) . '%s'", \ s_mark, empty(a:cmdl.cmd.args.string[2:]) ? \ '''''' : a:cmdl.cmd.args.string[2:], s_mark) else " make ending single backslash literal or else it will escape s_mark if substitute(a:cmdl.cmd.args.string, '\\\\', '', 'g') =~# '\\$' let cmd_str .= s_mark . a:cmdl.cmd.args.string . '\' . s_mark else let cmd_str .= s_mark . a:cmdl.cmd.args.string . s_mark endif endif let cmd_str .= a:cmdl.cmd.args.delimiter let cmd_str .= substitute(a:cmdl.cmd.args.flags, '[^giI]', '', 'g') return cmd_str endfunction function! s:preview_window(range, pattern, type, preview_cmd) abort if !empty(getcmdwintype()) return endif " skip when arguments are unchanged if exists('s:buf[s:nr].preview_window.args') \ && s:buf[s:nr].preview_window.args ==# string(a:) return endif let winopen_pattern = '\v^\s*(((' \ . 'vert%[ical]|' \ . 'lefta%[bove]|' \ . 'abo%[veleft]|' \ . 'rightb%[elow]|' \ . 'bel%[owright]|' \ . 'to%[pleft]|' \ . 'bo%[tright]' \ . ')\s+)+)=(\d+\s*)=v=new\s*$' let winopen = '' try if g:traces_preview_window =~# winopen_pattern let winopen = g:traces_preview_window elseif eval(g:traces_preview_window) =~# winopen_pattern let winopen = eval(g:traces_preview_window) else return endif catch return endtry let range = a:range if a:type is 'g' && empty(range) let range = [0, line('$')] endif if empty(range) || empty(a:pattern) \ || range[0] >= line('w0') && range[1] <= line('w$') if exists('s:buf[s:nr].preview_window') let s:buf[s:nr].preview_window.args = string(a:) noautocmd call win_gotoid(s:buf[s:nr].preview_window.winid) noautocmd %delete noautocmd call win_gotoid(s:buf[s:nr].cur_win) endif return endif " prepare preview window if !exists('s:buf[s:nr].preview_window') execute 'noautocmd noswapfile' winopen noautocmd setlocal undolevels=-1 nobuflisted buftype=nofile bufhidden=wipe let s:buf[s:nr].preview_window = {} let s:buf[s:nr].preview_window.bufnr = bufnr('%') let s:buf[s:nr].preview_window.winid = win_getid() call matchadd('TracesReplace', s:buf[s:nr].s_mark . '\_.\{-}' . s:buf[s:nr].s_mark, 101, -1,) call matchadd('Conceal', s:buf[s:nr].s_mark . '\|' . s:buf[s:nr].s_mark, 102, -1) noautocmd setlocal conceallevel=2 noautocmd setlocal concealcursor=c noautocmd setlocal nocursorline nocursorcolumn nonumber norelativenumber noautocmd call win_gotoid(s:buf[s:nr].cur_win) endif let s:buf[s:nr].preview_window.args = string(a:) " gather lines for preview window let view = winsaveview() let wintop = line('w0') let winbot = line('w$') let filtered = [] let currentline = max([range[0], 1]) let stopline = range[1] let max = getwininfo(s:buf[s:nr].preview_window.winid)[0].height while currentline <= stopline && len(filtered) < max call cursor(currentline, 1) let matchstart = s:search(a:pattern, 'cn', stopline, s:search_timeout_remaining) if matchstart call cursor(matchstart, 1) let matchend = s:search(a:pattern, 'cen', stopline, s:search_timeout_remaining) let currentline = matchend + 1 if matchstart < wintop || matchstart > winbot call extend(filtered, getbufline('%', matchstart, matchend)) endif else break endif endwhile call winrestview(view) " fill and highlight preview window noautocmd call win_gotoid(s:buf[s:nr].preview_window.winid) noautocmd %delete noautocmd call append(0, filtered) silent! call matchdelete(s:buf[s:nr].preview_window.match_id) if a:type is 's' && !empty(a:preview_cmd) execute 'silent! %' a:preview_cmd else silent! let s:buf[s:nr].preview_window.match_id = \ matchadd('TracesSearch', a:pattern, 101, -1) endif normal! Gddgg noautocmd call win_gotoid(s:buf[s:nr].cur_win) endfunction function! s:preview_window_close() if !exists('s:buf[s:nr].preview_window') return endif execute 'noautocmd bwipe! ' s:buf[s:nr].preview_window.bufnr unlet s:buf[s:nr].preview_window endfunction function! s:preview_substitute(cmdl) abort if empty(a:cmdl.cmd.args) return endif let ptrn = a:cmdl.cmd.args.pattern let str = a:cmdl.cmd.args.string let range = a:cmdl.range.abs let dlm = a:cmdl.cmd.args.delimiter call s:pos_pattern(ptrn, range, dlm, 1) if !g:traces_substitute_preview || &readonly || !&modifiable || empty(str) call s:highlight('TracesSearch', s:add_hl_guard(ptrn, range, 's'), 101) call s:preview_window(range, ptrn, 's', '') return endif call s:save_undo_history() if s:buf[s:nr].undo_file is 0 call s:highlight('TracesSearch', s:add_hl_guard(ptrn, range, 's'), 101) call s:preview_window(range, ptrn, 's', '') return endif let range_str = s:format_range(a:cmdl) let cmd_str = s:format_command(a:cmdl) call s:preview_window(range, ptrn, 's', cmd_str) let cmd = 'noautocmd keepjumps keeppatterns ' . range_str . cmd_str let tick = b:changedtick let lines = line('$') let view = winsaveview() let ul = &l:undolevels noautocmd let &l:undolevels = 0 silent! execute cmd noautocmd let &l:undolevels = ul if tick == b:changedtick return endif let s:buf[s:nr].changed = 1 call winrestview(view) let s:redraw_later = 1 let lines = lines - line('$') if lines && !get(s:, 'entire_file') && !empty(range) if len(range) == 1 call add(range, range[0]) endif let range[-1] -= lines call s:highlight('Visual', s:get_selection_regexp(range), 100) endif call s:highlight('TracesSearch', '', 101) call s:highlight('TracesReplace', s:buf[s:nr].s_mark . '\_.\{-}' . s:buf[s:nr].s_mark, 101) call s:highlight('Conceal', s:buf[s:nr].s_mark . '\|' . s:buf[s:nr].s_mark, 102) endfunction function! s:preview_global(cmdl) abort if empty(a:cmdl.range.specifier) && has_key(a:cmdl.cmd.args, 'pattern') let pattern = a:cmdl.cmd.args.pattern let range = a:cmdl.range.abs call s:highlight('TracesSearch', s:add_hl_guard(pattern, range, 'g'), 101) call s:pos_pattern(pattern, range, a:cmdl.cmd.args.delimiter, 0) call s:preview_window(range, pattern, 'g', '') endif endfunction function! s:preview_sort(cmdl) abort if empty(a:cmdl.range.specifier) && has_key(a:cmdl.cmd.args, 'pattern') let pattern = a:cmdl.cmd.args.pattern let range = a:cmdl.range.abs call s:highlight('TracesSearch', s:add_hl_guard(pattern, range, 'g'), 101) call s:pos_pattern(pattern, range, a:cmdl.cmd.args.delimiter, 0) call s:preview_window(range, pattern, 'g', '') endif endfunction function! s:clear_cursors() abort if exists('g:traces_cursors') silent! call map(g:traces_cursors, 'matchdelete(v:val)') unlet g:traces_cursors endif endfunction function! s:preview_normal(cmdl) abort if !empty(getcmdwintype()) return endif let str = a:cmdl.cmd.args.string[0] if !g:traces_normal_preview || &readonly || !&modifiable || empty(str) \ || (!has("patch-8.2.2961") && !has('nvim')) call s:clear_cursors() return endif let cmd = a:cmdl.cmd.name let range = '' if len(a:cmdl.range.abs) == 1 let range .= a:cmdl.range.abs[0] elseif len(a:cmdl.range.abs) > 1 let range .= max([a:cmdl.range.abs[-2], line("w0")]) let range .= ';' let range .= min([a:cmdl.range.abs[-1], line("w$")]) endif call s:save_undo_history() if exists('g:traces_cursors') && !empty(g:traces_cursors) silent! call map(g:traces_cursors, 'matchdelete(v:val)') endif let g:traces_cursors = [] let tick = b:changedtick let winid = win_getid() let view = winsaveview() let ul = &l:undolevels noautocmd let &l:undolevels = 1 try execute 'silent noautocmd keepjumps' range . cmd str . "\call add(g:traces_cursors, matchaddpos('TracesCursor', [getcurpos()[1:2]], 101))\" catch finally execute "noautocmd keepjumps normal! \\" endtry noautocmd call win_gotoid(winid) noautocmd let &l:undolevels = ul call winrestview(view) " required to highlight EOL with matchaddpos() noautocmd setlocal list noautocmd setlocal listchars=eol:\ ,tab:\ \ noautocmd let &hlsearch = 0 let s:redraw_later = 1 call s:restore_marks() if tick == b:changedtick return endif let s:buf[s:nr].changed = 1 endfunction function! s:cmdl_enter(view) abort let s:buf[s:nr] = {} let s:buf[s:nr].search_cache = {} let s:buf[s:nr].view = winsaveview() let s:buf[s:nr].show_range = 0 let s:buf[s:nr].duration = 0 let s:buf[s:nr].hlsearch = &hlsearch let s:buf[s:nr].cword = expand('') let s:buf[s:nr].cWORD = expand('') let s:buf[s:nr].cfile = expand('') let s:buf[s:nr].cur_init_pos = [line('.'), col('.')] let s:buf[s:nr].seq_last = undotree().seq_last let s:buf[s:nr].empty_undotree = empty(undotree().entries) let s:buf[s:nr].changed = 0 let s:buf[s:nr].cmdheight = &cmdheight let s:buf[s:nr].redraw = 1 let s:buf[s:nr].s_mark = (&encoding == 'utf-8' ? "\uf8b4" : '' ) let s:buf[s:nr].cur_win = win_getid() let s:buf[s:nr].alt_win = win_getid(winnr('#')) let s:buf[s:nr].winwidth = &winwidth let s:buf[s:nr].winheight = &winheight let s:buf[s:nr].list = &l:list let s:buf[s:nr].listchars = &l:listchars let s:buf[s:nr].pre_cmdl_view = a:view let s:buf[s:nr].win = {} call s:save_marks() endfunction function! traces#cmdl_leave() abort let s:nr = bufnr('%') if !exists('s:buf[s:nr]') return endif call s:restore_undo_history() " clear highlights for id in keys(s:buf[s:nr].win) if empty(getcmdwintype()) && !s:has_matchdelete_win noautocmd call win_gotoid(id) endif for group in keys(get(s:buf[s:nr].win[id], 'matches', {})) if s:has_matchdelete_win silent! call matchdelete(s:buf[s:nr].win[id].matches[group].match_id, id) else silent! call matchdelete(s:buf[s:nr].win[id].matches[group].match_id) endif endfor endfor " restore previous window p if empty(getcmdwintype()) && win_getid(winnr('#')) isnot s:buf[s:nr].alt_win let winrestcmd = winrestcmd() noautocmd call win_gotoid(s:buf[s:nr].alt_win) noautocmd call win_gotoid(s:buf[s:nr].cur_win) execute winrestcmd endif " restore local options for id in keys(s:buf[s:nr].win) for option in keys(get(s:buf[s:nr].win[id], 'options', {})) call setwinvar(id, '&' . option, s:buf[s:nr].win[id].options[option]) endfor endfor " restore global options if &hlsearch !=# s:buf[s:nr].hlsearch noautocmd let &hlsearch = s:buf[s:nr].hlsearch endif if &cmdheight !=# s:buf[s:nr].cmdheight noautocmd let &cmdheight = s:buf[s:nr].cmdheight endif if &winwidth isnot s:buf[s:nr].winwidth noautocmd let &winwidth = s:buf[s:nr].winwidth endif if &winheight isnot s:buf[s:nr].winheight noautocmd let &winheight = s:buf[s:nr].winheight endif noautocmd let &l:list = s:buf[s:nr].list noautocmd let &l:listchars = s:buf[s:nr].listchars call s:clear_cursors() call s:preview_window_close() if winsaveview() !=# s:buf[s:nr].view call winrestview(s:buf[s:nr].view) endif unlet s:buf[s:nr] endfunction function! s:evaluate_cmdl(string) abort let cmdl = {} let cmdl.string = a:string let r = s:evaluate_range(s:parse_range([], cmdl.string)) let cmdl.range = {} let cmdl.range.abs = r.range let cmdl.range.end = r.end let cmdl.range.pattern = s:get_selection_regexp(r.range) let cmdl.range.specifier = s:add_hl_guard(s:add_opt(r.pattern, cmdl), r.end, 'r') let cmdl.cmd = {} let cmdl.cmd.args = {} call s:parse_command(cmdl) return cmdl endfunction function! s:save_marks() abort if !exists('s:buf[s:nr].marks') let types = ['[', ']', '<', '>'] let s:buf[s:nr].marks = {} for mark in types let s:buf[s:nr].marks[mark] = getpos("'" . mark) endfor endif endfunction function! s:restore_marks() abort if exists('s:buf[s:nr].marks') for mark in keys(s:buf[s:nr].marks) call setpos("'" . mark, s:buf[s:nr].marks[mark]) endfor endif endfunction function! s:save_undo_history() abort if exists('s:buf[s:nr].undo_file') return endif if !empty(getcmdwintype()) || s:buf[s:nr].empty_undotree let s:buf[s:nr].undo_file = 1 return endif let s:buf[s:nr].undo_file = tempname() let time = reltime() noautocmd silent execute 'wundo ' . s:buf[s:nr].undo_file let s:wundo_time = reltimefloat(reltime(time)) * 1000 if !filereadable(s:buf[s:nr].undo_file) let s:buf[s:nr].undo_file = 0 return endif endfunction function! s:restore_undo_history() abort if s:buf[s:nr].changed noautocmd keepjumps silent undo call s:restore_marks() endif if type(get(s:buf[s:nr], 'undo_file')) isnot v:t_string return endif if has('nvim') " can't use try/catch on Neovim inside CmdlineLeave " https://github.com/neovim/neovim/issues/7876 silent! execute 'noautocmd rundo ' . s:buf[s:nr].undo_file if undotree().seq_last !=# s:buf[s:nr].seq_last echohl WarningMsg echom 'traces.vim - undo history could not be restored' echohl None endif else try silent execute 'noautocmd rundo ' . s:buf[s:nr].undo_file catch echohl WarningMsg echom 'traces.vim - ' . v:exception echohl None endtry endif call delete(s:buf[s:nr].undo_file) endfunction function! s:adjust_cmdheight() abort let len = strwidth(strtrans(getcmdline())) + 2 let col = &columns let height = &cmdheight if col * height < len noautocmd let &cmdheight=(len / col) + 1 redraw elseif col * (height - 1) >= len && height > s:buf[s:nr].cmdheight noautocmd let &cmdheight=max([(len / col), s:buf[s:nr].cmdheight]) redraw endif endfunction function! s:skip_modifiers(cmdl) abort let cmdl = a:cmdl " skip leading colons let cmdl = substitute(cmdl, '\v^[[:space:]:]+', '', '') " skip modifiers let pattern = '\v^%(%(' \ . 'sil%[ent]%(\!|\H@=)|' \ . 'verb%[ose]\H@=|' \ . 'noa%[utocmd]\H@=|' \ . 'loc%[kmarks]\H@=|' \ . 'keepp%[atterns]\H@=|' \ . 'keepa%[lt]\H@=|' \ . 'keepj%[umps]\H@=|' \ . 'kee%[pmarks]\H@=' \ . ')\s*)+' let cmdl = substitute(cmdl, pattern, '', '') if g:traces_skip_modifiers " skip *do modifiers let cmdl = substitute(cmdl, \ '\v^%(%(%(\d+|\.|\$|\%)\s*[,;]=\s*)+)=\s*%(cdo|cfdo|ld%[o]|lfdo' \ . '|bufd%[o]|tabd%[o]\!@!|argdo|wind%[o]\!@!)%(\!|\H@=)\s*', '', '') " skip modifiers again let cmdl = substitute(cmdl, pattern, '', '') endif return cmdl endfunction function! traces#init(cmdl, view) abort let s:nr = bufnr('%') if !exists('s:buf[s:nr]') call s:cmdl_enter(a:view) endif let s:redraw_later = 0 let s:moved = 0 let s:last_pattern = @/ let s:specifier_delimiter = 0 let s:wundo_time = 0 let s:search_timeout_remaining = s:search_timeout if s:buf[s:nr].duration < s:timeout let start_time = reltime() endif if s:buf[s:nr].changed let view = winsaveview() noautocmd keepjumps silent undo let s:buf[s:nr].changed = 0 let s:redraw_later = 1 call s:restore_marks() call winrestview(view) endif if s:buf[s:nr].duration < s:timeout let cmdl = s:evaluate_cmdl([s:skip_modifiers(a:cmdl)]) " range preview if (!empty(cmdl.cmd.name) && !empty(cmdl.cmd.args) || s:buf[s:nr].show_range \ || s:specifier_delimiter && g:traces_num_range_preview) \ && !get(s:, 'entire_file') call s:highlight('Visual', cmdl.range.pattern, 100) if empty(cmdl.cmd.name) call s:preview_window_close() call s:highlight('TracesSearch', cmdl.range.specifier, 101) endif call s:pos_range(cmdl.range.end, cmdl.range.specifier) else " clear unnecessary range hl call s:highlight('Visual', '', 100) endif " cmd preview if cmdl.cmd.name =~# '\v^%(s%[ubstitute]|sm%[agic]|sno%[magic])$' call s:preview_substitute(cmdl) elseif cmdl.cmd.name =~# '\v^%(g%[lobal]\!=|v%[global])$' call s:preview_global(cmdl) elseif cmdl.cmd.name =~# '\v^%(sor%[t]\!=)$' call s:preview_sort(cmdl) elseif cmdl.cmd.name =~# '\v^norm%[al]\!=$' call s:preview_normal(cmdl) endif if empty(cmdl.cmd.name) && empty(cmdl.range.specifier) \ || !empty(cmdl.cmd.name) && empty(cmdl.cmd.args) call s:highlight('TracesSearch', '', 101) call s:preview_window_close() call s:clear_cursors() endif else call s:preview_window_close() endif " move to starting position if necessary if !s:moved && winsaveview() != s:buf[s:nr].view && !wildmenumode() call winrestview(s:buf[s:nr].view) endif " redraw screen if necessary if s:redraw_later && !wildmenumode() call s:adjust_cmdheight() if has('nvim') redraw else " https://github.com/markonm/traces.vim/issues/17 " if Vim is missing CmdlineChanged, use explicit redraw only at the " start of preview or else it is going to be slow if exists('##CmdlineChanged') || s:buf[s:nr].redraw redraw let s:buf[s:nr].redraw = 0 else call winline() endif " after patch 8.0.1449, necessary for linux cui, otherwise highlighting " is not drawn properly, fixed by 8.0.1476 if has('unix') && !has('gui_running') && has("patch-8.0.1449") && !has("patch-8.0.1476") silent! call feedkeys(getcmdpos() is 1 ? "\\" : "\\", 'tn') endif endif endif if exists('start_time') let s:buf[s:nr].duration = reltimefloat(reltime(start_time)) * 1000 - s:wundo_time endif endfunction function! traces#get_cword() abort return s:buf[s:nr].cword endfunction function! traces#get_cWORD() abort return s:buf[s:nr].cWORD endfunction function! traces#get_cfile() abort return s:buf[s:nr].cfile endfunction function! traces#get_pfile() abort let result = split(globpath(&path, s:buf[s:nr].cfile), '\n') if len(result) && len(s:buf[s:nr].cfile) return result[-1] endif return '' endfunction function! traces#check_b() abort let s:nr = bufnr('%') if getcmdtype() == ':' && exists('s:buf[s:nr]') return 1 endif endfunction let &cpo = s:cpo_save unlet s:cpo_save