" Script Name: mark.vim " Description: Highlight several words in different colors simultaneously. " " Copyright: (C) 2005-2008 by Yuheng Xie " (C) 2008-2010 by Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " Dependencies: " - SearchSpecial.vim autoload script (optional, for improved search messages). " " Version: 2.4.0 " Changes: " 13-Jul-2010, Ingo Karkat " - ENH: The MarkSearch mappings ([*#/?]) add the original cursor " position to the jump list, like the built-in [/?*#nN] commands. This allows " to use the regular jump commands for mark matches, like with regular search " matches. " " 19-Feb-2010, Andy Wokula " - BUG: Clearing of an accidental zero-width match (e.g. via :Mark \zs) results " in endless loop. Thanks to Andy Wokula for the patch. " " 17-Nov-2009, Ingo Karkat + Andy Wokula " - BUG: Creation of literal pattern via '\V' in {Visual}m mapping " collided with individual escaping done in m mapping so that an " escaped '\*' would be interpreted as a multi item when both modes are used " for marking. Replaced \V with s:EscapeText() to be consistent. Replaced the " (overly) generic mark#GetVisualSelectionEscaped() with " mark#GetVisualSelectionAsRegexp() and " mark#GetVisualSelectionAsLiteralPattern(). Thanks to Andy Wokula for the " patch. " " 06-Jul-2009, Ingo Karkat " - Re-wrote s:AnyMark() in functional programming style. " - Now resetting 'smartcase' before the search, this setting should not be " considered for *-command-alike searches and cannot be supported because all " mark patterns are concatenated into one large regexp, anyway. " " 04-Jul-2009, Ingo Karkat " - Re-wrote s:Search() to handle v:count: " - Obsoleted s:current_mark_position; mark#CurrentMark() now returns both the " mark text and start position. " - s:Search() now checks for a jump to the current mark during a backward " search; this eliminates a lot of logic at its calling sites. " - Reverted negative logic at calling sites; using empty() instead of != "". " - Now passing a:isBackward instead of optional flags into s:Search() and " around its callers. " - ':normal! zv' moved from callers into s:Search(). " - Removed delegation to SearchSpecial#ErrorMessage(), because the fallback " implementation is perfectly fine and the SearchSpecial routine changed its " output format into something unsuitable anyway. " - Using descriptive text instead of "@" (and appropriate highlighting) when " querying for the pattern to mark. " " 02-Jul-2009, Ingo Karkat " - Split off functions into autoload script. "- functions ------------------------------------------------------------------ function! s:EscapeText( text ) return substitute( escape(a:text, '\' . '^$.*[~'), "\n", '\\n', 'ge' ) endfunction " Mark the current word, like the built-in star command. " If the cursor is on an existing mark, remove it. function! mark#MarkCurrentWord() let l:regexp = mark#CurrentMark()[0] if empty(l:regexp) let l:cword = expand("") " The star command only creates a \ search pattern if the " actually only consists of keyword characters. if l:cword =~# '^\k\+$' let l:regexp = '\<' . s:EscapeText(l:cword) . '\>' elseif l:cword != '' let l:regexp = s:EscapeText(l:cword) endif endif if ! empty(l:regexp) call mark#DoMark(l:regexp) endif endfunction function! s:GetVisualSelection() let save_a = @a silent normal! gv"ay let res = @a let @a = save_a return res endfunction function! mark#GetVisualSelectionAsLiteralPattern() return s:EscapeText(s:GetVisualSelection()) endfunction function! mark#GetVisualSelectionAsRegexp() return substitute(s:GetVisualSelection(), '\n', '', 'g') endfunction " Manually input a regular expression. function! mark#MarkRegex( regexpPreset ) call inputsave() echohl Question let l:regexp = input('Input pattern to mark: ', a:regexpPreset) echohl None call inputrestore() if ! empty(l:regexp) call mark#DoMark(l:regexp) endif endfunction function! s:Cycle( ... ) let l:currentCycle = g:mwCycle let l:newCycle = (a:0 ? a:1 : g:mwCycle) + 1 let g:mwCycle = (l:newCycle < g:mwCycleMax ? l:newCycle : 0) return l:currentCycle endfunction " Set / clear matches in the current window. function! s:MarkMatch( indices, expr ) for l:index in a:indices if w:mwMatch[l:index] > 0 silent! call matchdelete(w:mwMatch[l:index]) let w:mwMatch[l:index] = 0 endif endfor if ! empty(a:expr) " Make the match according to the 'ignorecase' setting, like the star command. " (But honor an explicit case-sensitive regexp via the /\C/ atom.) let l:expr = ((&ignorecase && a:expr !~# '\\\@= 0 call histadd("/", regexp) endif if stridx(g:mwHistAdd, "@") >= 0 call histadd("@", regexp) endif " choose an unused mark group let i = 0 while i < g:mwCycleMax if empty(g:mwWord[i]) let g:mwWord[i] = regexp call s:Cycle(i) call s:MarkScope([i], regexp) return endif let i += 1 endwhile " choose a mark group by cycle let i = s:Cycle() if g:mwLastSearched == g:mwWord[i] let g:mwLastSearched = '' endif let g:mwWord[i] = regexp call s:MarkScope([i], regexp) endfunction " Initialize mark colors in a (new) window. function! mark#UpdateMark() if ! exists('w:mwMatch') let w:mwMatch = repeat([0], g:mwCycleMax) endif let i = 0 while i < g:mwCycleMax if empty(g:mwWord[i]) call s:MarkMatch([i], '') else call s:MarkMatch([i], g:mwWord[i]) endif let i += 1 endwhile endfunction " Return [mark text, mark start position] of the mark under the cursor (or " ['', []] if there is no mark); multi-lines marks not supported. function! mark#CurrentMark() let line = getline(".") let i = 0 while i < g:mwCycleMax if !empty(g:mwWord[i]) " Note: col() is 1-based, all other indexes zero-based! let start = 0 while start >= 0 && start < strlen(line) && start < col(".") let b = match(line, g:mwWord[i], start) let e = matchend(line, g:mwWord[i], start) if b < col(".") && col(".") <= e return [g:mwWord[i], [line("."), (b + 1)]] endif if b == e break endif let start = e endwhile endif let i += 1 endwhile return ['', []] endfunction " Search current mark. function! mark#SearchCurrentMark( isBackward ) let [l:markText, l:markPosition] = mark#CurrentMark() if empty(l:markText) if empty(g:mwLastSearched) call mark#SearchAnyMark(a:isBackward) let g:mwLastSearched = mark#CurrentMark()[0] else call s:Search(g:mwLastSearched, a:isBackward, [], 'same-mark') endif else call s:Search(l:markText, a:isBackward, l:markPosition, (l:markText ==# g:mwLastSearched ? 'same-mark' : 'new-mark')) let g:mwLastSearched = l:markText endif endfunction silent! call SearchSpecial#DoesNotExist() " Execute a function to force autoload. if exists('*SearchSpecial#WrapMessage') function! s:WrapMessage( searchType, searchPattern, isBackward ) redraw call SearchSpecial#WrapMessage(a:searchType, a:searchPattern, a:isBackward) endfunction function! s:EchoSearchPattern( searchType, searchPattern, isBackward ) call SearchSpecial#EchoSearchPattern(a:searchType, a:searchPattern, a:isBackward) endfunction else function! s:Trim( message ) " Limit length to avoid "Hit ENTER" prompt. return strpart(a:message, 0, (&columns / 2)) . (len(a:message) > (&columns / 2) ? "..." : "") endfunction function! s:WrapMessage( searchType, searchPattern, isBackward ) redraw let v:warningmsg = printf('%s search hit %s, continuing at %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), (a:isBackward ? 'BOTTOM' : 'TOP')) echohl WarningMsg echo s:Trim(v:warningmsg) echohl None endfunction function! s:EchoSearchPattern( searchType, searchPattern, isBackward ) let l:message = (a:isBackward ? '?' : '/') . a:searchPattern echohl SearchSpecialSearchType echo a:searchType echohl None echon s:Trim(l:message) endfunction endif function! s:ErrorMessage( searchType, searchPattern, isBackward ) if &wrapscan let v:errmsg = a:searchType . ' not found: ' . a:searchPattern else let v:errmsg = printf('%s search hit %s without match for: %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), a:searchPattern) endif echohl ErrorMsg echomsg v:errmsg echohl None endfunction " Wrapper around search() with additonal search and error messages and "wrapscan" warning. function! s:Search( pattern, isBackward, currentMarkPosition, searchType ) let l:save_view = winsaveview() " searchpos() obeys the 'smartcase' setting; however, this setting doesn't " make sense for the mark search, because all patterns for the marks are " concatenated as branches in one large regexp, and because patterns that " result from the *-command-alike mappings should not obey 'smartcase' (like " the * command itself), anyway. If the :Mark command wants to support " 'smartcase', it'd have to emulate that into the regular expression. let l:save_smartcase = &smartcase set nosmartcase let l:count = v:count1 let [l:startLine, l:startCol] = [line('.'), col('.')] let l:isWrapped = 0 let l:isMatch = 0 let l:line = 0 while l:count > 0 " Search for next match, 'wrapscan' applies. let [l:line, l:col] = searchpos( a:pattern, (a:isBackward ? 'b' : '') ) "****D echomsg '****' a:isBackward string([l:line, l:col]) string(a:currentMarkPosition) l:count if a:isBackward && l:line > 0 && [l:line, l:col] == a:currentMarkPosition && l:count == v:count1 " On a search in backward direction, the first match is the start of the " current mark (if the cursor was positioned on the current mark text, and " not at the start of the mark text). " In contrast to the normal search, this is not considered the first " match. The mark text is one entity; if the cursor is positioned anywhere " inside the mark text, the mark text is considered the current mark. The " built-in '*' and '#' commands behave in the same way; the entire " text is considered the current match, and jumps move outside that text. " In normal search, the cursor can be positioned anywhere (via offsets) " around the search, and only that single cursor position is considered " the current match. " Thus, the search is retried without a decrease of l:count, but only if " this was the first match; repeat visits during wrapping around count as " a regular match. The search also must not be retried when this is the " first match, but we've been here before (i.e. l:isMatch is set): This " means that there is only the current mark in the buffer, and we must " break out of the loop and indicate that no other mark was found. if l:isMatch let l:line = 0 break endif " The l:isMatch flag is set so if the final mark cannot be reached, the " original cursor position is restored. This flag also allows us to detect " whether we've been here before, which is checked above. let l:isMatch = 1 elseif l:line > 0 let l:isMatch = 1 let l:count -= 1 " Note: No need to check 'wrapscan'; the wrapping can only occur if " 'wrapscan' is actually on. if ! a:isBackward && (l:startLine > l:line || l:startLine == l:line && l:startCol >= l:col) let l:isWrapped = 1 elseif a:isBackward && (l:startLine < l:line || l:startLine == l:line && l:startCol <= l:col) let l:isWrapped = 1 endif else break endif endwhile let &smartcase = l:save_smartcase " We're not stuck when the search wrapped around and landed on the current " mark; that's why we exclude a possible wrap-around via v:count1 == 1. let l:isStuckAtCurrentMark = ([l:line, l:col] == a:currentMarkPosition && v:count1 == 1) if l:line > 0 && ! l:isStuckAtCurrentMark let l:matchPosition = getpos('.') " Open fold at the search result, like the built-in commands. normal! zv " Add the original cursor position to the jump list, like the " [/?*#nN] commands. " Implementation: Memorize the match position, restore the view to the state " before the search, then jump straight back to the match position. This " also allows us to set a jump only if a match was found. (:call " setpos("''", ...) doesn't work in Vim 7.2) call winrestview(l:save_view) normal! m' call setpos('.', l:matchPosition) if l:isWrapped call s:WrapMessage(a:searchType, a:pattern, a:isBackward) else call s:EchoSearchPattern(a:searchType, a:pattern, a:isBackward) endif return 1 else if l:isMatch " The view has been changed by moving through matches until the end / " start of file, when 'nowrapscan' forced a stop of searching before the " l:count'th match was found. " Restore the view to the state before the search. call winrestview(l:save_view) endif call s:ErrorMessage(a:searchType, a:pattern, a:isBackward) return 0 endif endfunction " Combine all marks into one regexp. function! s:AnyMark() return join(filter(copy(g:mwWord), '! empty(v:val)'), '\|') endfunction " Search any mark. function! mark#SearchAnyMark( isBackward ) let l:markPosition = mark#CurrentMark()[1] let l:markText = s:AnyMark() call s:Search(l:markText, a:isBackward, l:markPosition, 'any-mark') let g:mwLastSearched = "" endfunction " Search last searched mark. function! mark#SearchNext( isBackward ) let l:markText = mark#CurrentMark()[0] if empty(l:markText) return 0 else if empty(g:mwLastSearched) call mark#SearchAnyMark(a:isBackward) else call mark#SearchCurrentMark(a:isBackward) endif return 1 endif endfunction "- initializations ------------------------------------------------------------ augroup Mark autocmd! autocmd VimEnter * if ! exists('w:mwMatch') | call mark#UpdateMark() | endif autocmd WinEnter * if ! exists('w:mwMatch') | call mark#UpdateMark() | endif autocmd TabEnter * call mark#UpdateScope() augroup END " Define global variables and initialize current scope. function! s:InitMarkVariables() if !exists("g:mwHistAdd") let g:mwHistAdd = "/@" endif if !exists("g:mwCycleMax") let i = 1 while hlexists("MarkWord" . i) let i = i + 1 endwhile let g:mwCycleMax = i - 1 endif if !exists("g:mwCycle") let g:mwCycle = 0 endif if !exists("g:mwWord") let g:mwWord = repeat([''], g:mwCycleMax) endif if !exists("g:mwLastSearched") let g:mwLastSearched = "" endif endfunction call s:InitMarkVariables() call mark#UpdateScope() " vim: ts=2 sw=2