1 " Script Name: mark.vim
2 " Description: Highlight several words in different colors simultaneously.
4 " Copyright: (C) 2005-2008 by Yuheng Xie
5 " (C) 2008-2010 by Ingo Karkat
6 " The VIM LICENSE applies to this script; see ':help copyright'.
8 " Maintainer: Ingo Karkat <ingo@karkat.de>
11 " - SearchSpecial.vim autoload script (optional, for improved search messages).
15 " 13-Jul-2010, Ingo Karkat
16 " - ENH: The MarkSearch mappings (<Leader>[*#/?]) add the original cursor
17 " position to the jump list, like the built-in [/?*#nN] commands. This allows
18 " to use the regular jump commands for mark matches, like with regular search
21 " 19-Feb-2010, Andy Wokula
22 " - BUG: Clearing of an accidental zero-width match (e.g. via :Mark \zs) results
23 " in endless loop. Thanks to Andy Wokula for the patch.
25 " 17-Nov-2009, Ingo Karkat + Andy Wokula
26 " - BUG: Creation of literal pattern via '\V' in {Visual}<Leader>m mapping
27 " collided with individual escaping done in <Leader>m mapping so that an
28 " escaped '\*' would be interpreted as a multi item when both modes are used
29 " for marking. Replaced \V with s:EscapeText() to be consistent. Replaced the
30 " (overly) generic mark#GetVisualSelectionEscaped() with
31 " mark#GetVisualSelectionAsRegexp() and
32 " mark#GetVisualSelectionAsLiteralPattern(). Thanks to Andy Wokula for the
35 " 06-Jul-2009, Ingo Karkat
36 " - Re-wrote s:AnyMark() in functional programming style.
37 " - Now resetting 'smartcase' before the search, this setting should not be
38 " considered for *-command-alike searches and cannot be supported because all
39 " mark patterns are concatenated into one large regexp, anyway.
41 " 04-Jul-2009, Ingo Karkat
42 " - Re-wrote s:Search() to handle v:count:
43 " - Obsoleted s:current_mark_position; mark#CurrentMark() now returns both the
44 " mark text and start position.
45 " - s:Search() now checks for a jump to the current mark during a backward
46 " search; this eliminates a lot of logic at its calling sites.
47 " - Reverted negative logic at calling sites; using empty() instead of != "".
48 " - Now passing a:isBackward instead of optional flags into s:Search() and
50 " - ':normal! zv' moved from callers into s:Search().
51 " - Removed delegation to SearchSpecial#ErrorMessage(), because the fallback
52 " implementation is perfectly fine and the SearchSpecial routine changed its
53 " output format into something unsuitable anyway.
54 " - Using descriptive text instead of "@" (and appropriate highlighting) when
55 " querying for the pattern to mark.
57 " 02-Jul-2009, Ingo Karkat
58 " - Split off functions into autoload script.
60 "- functions ------------------------------------------------------------------
61 function! s:EscapeText( text )
62 return substitute( escape(a:text, '\' . '^$.*[~'), "\n", '\\n', 'ge' )
64 " Mark the current word, like the built-in star command.
65 " If the cursor is on an existing mark, remove it.
66 function! mark#MarkCurrentWord()
67 let l:regexp = mark#CurrentMark()[0]
69 let l:cword = expand("<cword>")
71 " The star command only creates a \<whole word\> search pattern if the
72 " <cword> actually only consists of keyword characters.
73 if l:cword =~# '^\k\+$'
74 let l:regexp = '\<' . s:EscapeText(l:cword) . '\>'
76 let l:regexp = s:EscapeText(l:cword)
81 call mark#DoMark(l:regexp)
85 function! s:GetVisualSelection()
92 function! mark#GetVisualSelectionAsLiteralPattern()
93 return s:EscapeText(s:GetVisualSelection())
95 function! mark#GetVisualSelectionAsRegexp()
96 return substitute(s:GetVisualSelection(), '\n', '', 'g')
99 " Manually input a regular expression.
100 function! mark#MarkRegex( regexpPreset )
103 let l:regexp = input('Input pattern to mark: ', a:regexpPreset)
107 call mark#DoMark(l:regexp)
111 function! s:Cycle( ... )
112 let l:currentCycle = g:mwCycle
113 let l:newCycle = (a:0 ? a:1 : g:mwCycle) + 1
114 let g:mwCycle = (l:newCycle < g:mwCycleMax ? l:newCycle : 0)
115 return l:currentCycle
118 " Set / clear matches in the current window.
119 function! s:MarkMatch( indices, expr )
120 for l:index in a:indices
121 if w:mwMatch[l:index] > 0
122 silent! call matchdelete(w:mwMatch[l:index])
123 let w:mwMatch[l:index] = 0
128 " Make the match according to the 'ignorecase' setting, like the star command.
129 " (But honor an explicit case-sensitive regexp via the /\C/ atom.)
130 let l:expr = ((&ignorecase && a:expr !~# '\\\@<!\\C') ? '\c' . a:expr : a:expr)
132 " Info: matchadd() does not consider the 'magic' (it's always on),
133 " 'ignorecase' and 'smartcase' settings.
134 let w:mwMatch[a:indices[0]] = matchadd('MarkWord' . (a:indices[0] + 1), l:expr, -10)
137 " Set / clear matches in all windows.
138 function! s:MarkScope( indices, expr )
139 let l:currentWinNr = winnr()
141 " By entering a window, its height is potentially increased from 0 to 1 (the
142 " minimum for the current window). To avoid any modification, save the window
143 " sizes and restore them after visiting all windows.
144 let l:originalWindowLayout = winrestcmd()
146 noautocmd windo call s:MarkMatch(a:indices, a:expr)
147 execute l:currentWinNr . 'wincmd w'
148 silent! execute l:originalWindowLayout
150 " Update matches in all windows.
151 function! mark#UpdateScope()
152 let l:currentWinNr = winnr()
154 " By entering a window, its height is potentially increased from 0 to 1 (the
155 " minimum for the current window). To avoid any modification, save the window
156 " sizes and restore them after visiting all windows.
157 let l:originalWindowLayout = winrestcmd()
159 noautocmd windo call mark#UpdateMark()
160 execute l:currentWinNr . 'wincmd w'
161 silent! execute l:originalWindowLayout
163 " Mark or unmark a regular expression.
164 function! mark#DoMark(...) " DoMark(regexp)
165 let regexp = (a:0 ? a:1 : '')
167 " clear all marks if regexp is null
171 while i < g:mwCycleMax
172 if !empty(g:mwWord[i])
178 let g:mwLastSearched = ""
179 call s:MarkScope(l:indices, '')
183 " clear the mark if it has been marked
185 while i < g:mwCycleMax
186 if regexp == g:mwWord[i]
187 if g:mwLastSearched == g:mwWord[i]
188 let g:mwLastSearched = ''
191 call s:MarkScope([i], '')
198 if stridx(g:mwHistAdd, "/") >= 0
199 call histadd("/", regexp)
201 if stridx(g:mwHistAdd, "@") >= 0
202 call histadd("@", regexp)
205 " choose an unused mark group
207 while i < g:mwCycleMax
208 if empty(g:mwWord[i])
209 let g:mwWord[i] = regexp
211 call s:MarkScope([i], regexp)
217 " choose a mark group by cycle
219 if g:mwLastSearched == g:mwWord[i]
220 let g:mwLastSearched = ''
222 let g:mwWord[i] = regexp
223 call s:MarkScope([i], regexp)
225 " Initialize mark colors in a (new) window.
226 function! mark#UpdateMark()
227 if ! exists('w:mwMatch')
228 let w:mwMatch = repeat([0], g:mwCycleMax)
232 while i < g:mwCycleMax
233 if empty(g:mwWord[i])
234 call s:MarkMatch([i], '')
236 call s:MarkMatch([i], g:mwWord[i])
242 " Return [mark text, mark start position] of the mark under the cursor (or
243 " ['', []] if there is no mark); multi-lines marks not supported.
244 function! mark#CurrentMark()
245 let line = getline(".")
247 while i < g:mwCycleMax
248 if !empty(g:mwWord[i])
249 " Note: col() is 1-based, all other indexes zero-based!
251 while start >= 0 && start < strlen(line) && start < col(".")
252 let b = match(line, g:mwWord[i], start)
253 let e = matchend(line, g:mwWord[i], start)
254 if b < col(".") && col(".") <= e
255 return [g:mwWord[i], [line("."), (b + 1)]]
268 " Search current mark.
269 function! mark#SearchCurrentMark( isBackward )
270 let [l:markText, l:markPosition] = mark#CurrentMark()
272 if empty(g:mwLastSearched)
273 call mark#SearchAnyMark(a:isBackward)
274 let g:mwLastSearched = mark#CurrentMark()[0]
276 call s:Search(g:mwLastSearched, a:isBackward, [], 'same-mark')
279 call s:Search(l:markText, a:isBackward, l:markPosition, (l:markText ==# g:mwLastSearched ? 'same-mark' : 'new-mark'))
280 let g:mwLastSearched = l:markText
284 silent! call SearchSpecial#DoesNotExist() " Execute a function to force autoload.
285 if exists('*SearchSpecial#WrapMessage')
286 function! s:WrapMessage( searchType, searchPattern, isBackward )
288 call SearchSpecial#WrapMessage(a:searchType, a:searchPattern, a:isBackward)
290 function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
291 call SearchSpecial#EchoSearchPattern(a:searchType, a:searchPattern, a:isBackward)
294 function! s:Trim( message )
295 " Limit length to avoid "Hit ENTER" prompt.
296 return strpart(a:message, 0, (&columns / 2)) . (len(a:message) > (&columns / 2) ? "..." : "")
298 function! s:WrapMessage( searchType, searchPattern, isBackward )
300 let v:warningmsg = printf('%s search hit %s, continuing at %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), (a:isBackward ? 'BOTTOM' : 'TOP'))
302 echo s:Trim(v:warningmsg)
305 function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
306 let l:message = (a:isBackward ? '?' : '/') . a:searchPattern
307 echohl SearchSpecialSearchType
310 echon s:Trim(l:message)
313 function! s:ErrorMessage( searchType, searchPattern, isBackward )
315 let v:errmsg = a:searchType . ' not found: ' . a:searchPattern
317 let v:errmsg = printf('%s search hit %s without match for: %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), a:searchPattern)
324 " Wrapper around search() with additonal search and error messages and "wrapscan" warning.
325 function! s:Search( pattern, isBackward, currentMarkPosition, searchType )
326 let l:save_view = winsaveview()
328 " searchpos() obeys the 'smartcase' setting; however, this setting doesn't
329 " make sense for the mark search, because all patterns for the marks are
330 " concatenated as branches in one large regexp, and because patterns that
331 " result from the *-command-alike mappings should not obey 'smartcase' (like
332 " the * command itself), anyway. If the :Mark command wants to support
333 " 'smartcase', it'd have to emulate that into the regular expression.
334 let l:save_smartcase = &smartcase
337 let l:count = v:count1
338 let [l:startLine, l:startCol] = [line('.'), col('.')]
343 " Search for next match, 'wrapscan' applies.
344 let [l:line, l:col] = searchpos( a:pattern, (a:isBackward ? 'b' : '') )
346 "****D echomsg '****' a:isBackward string([l:line, l:col]) string(a:currentMarkPosition) l:count
347 if a:isBackward && l:line > 0 && [l:line, l:col] == a:currentMarkPosition && l:count == v:count1
348 " On a search in backward direction, the first match is the start of the
349 " current mark (if the cursor was positioned on the current mark text, and
350 " not at the start of the mark text).
351 " In contrast to the normal search, this is not considered the first
352 " match. The mark text is one entity; if the cursor is positioned anywhere
353 " inside the mark text, the mark text is considered the current mark. The
354 " built-in '*' and '#' commands behave in the same way; the entire <cword>
355 " text is considered the current match, and jumps move outside that text.
356 " In normal search, the cursor can be positioned anywhere (via offsets)
357 " around the search, and only that single cursor position is considered
359 " Thus, the search is retried without a decrease of l:count, but only if
360 " this was the first match; repeat visits during wrapping around count as
361 " a regular match. The search also must not be retried when this is the
362 " first match, but we've been here before (i.e. l:isMatch is set): This
363 " means that there is only the current mark in the buffer, and we must
364 " break out of the loop and indicate that no other mark was found.
370 " The l:isMatch flag is set so if the final mark cannot be reached, the
371 " original cursor position is restored. This flag also allows us to detect
372 " whether we've been here before, which is checked above.
378 " Note: No need to check 'wrapscan'; the wrapping can only occur if
379 " 'wrapscan' is actually on.
380 if ! a:isBackward && (l:startLine > l:line || l:startLine == l:line && l:startCol >= l:col)
382 elseif a:isBackward && (l:startLine < l:line || l:startLine == l:line && l:startCol <= l:col)
389 let &smartcase = l:save_smartcase
391 " We're not stuck when the search wrapped around and landed on the current
392 " mark; that's why we exclude a possible wrap-around via v:count1 == 1.
393 let l:isStuckAtCurrentMark = ([l:line, l:col] == a:currentMarkPosition && v:count1 == 1)
394 if l:line > 0 && ! l:isStuckAtCurrentMark
395 let l:matchPosition = getpos('.')
397 " Open fold at the search result, like the built-in commands.
400 " Add the original cursor position to the jump list, like the
402 " Implementation: Memorize the match position, restore the view to the state
403 " before the search, then jump straight back to the match position. This
404 " also allows us to set a jump only if a match was found. (:call
405 " setpos("''", ...) doesn't work in Vim 7.2)
406 call winrestview(l:save_view)
408 call setpos('.', l:matchPosition)
411 call s:WrapMessage(a:searchType, a:pattern, a:isBackward)
413 call s:EchoSearchPattern(a:searchType, a:pattern, a:isBackward)
418 " The view has been changed by moving through matches until the end /
419 " start of file, when 'nowrapscan' forced a stop of searching before the
420 " l:count'th match was found.
421 " Restore the view to the state before the search.
422 call winrestview(l:save_view)
424 call s:ErrorMessage(a:searchType, a:pattern, a:isBackward)
429 " Combine all marks into one regexp.
430 function! s:AnyMark()
431 return join(filter(copy(g:mwWord), '! empty(v:val)'), '\|')
435 function! mark#SearchAnyMark( isBackward )
436 let l:markPosition = mark#CurrentMark()[1]
437 let l:markText = s:AnyMark()
438 call s:Search(l:markText, a:isBackward, l:markPosition, 'any-mark')
439 let g:mwLastSearched = ""
442 " Search last searched mark.
443 function! mark#SearchNext( isBackward )
444 let l:markText = mark#CurrentMark()[0]
448 if empty(g:mwLastSearched)
449 call mark#SearchAnyMark(a:isBackward)
451 call mark#SearchCurrentMark(a:isBackward)
457 "- initializations ------------------------------------------------------------
460 autocmd VimEnter * if ! exists('w:mwMatch') | call mark#UpdateMark() | endif
461 autocmd WinEnter * if ! exists('w:mwMatch') | call mark#UpdateMark() | endif
462 autocmd TabEnter * call mark#UpdateScope()
465 " Define global variables and initialize current scope.
466 function! s:InitMarkVariables()
467 if !exists("g:mwHistAdd")
468 let g:mwHistAdd = "/@"
470 if !exists("g:mwCycleMax")
472 while hlexists("MarkWord" . i)
475 let g:mwCycleMax = i - 1
477 if !exists("g:mwCycle")
480 if !exists("g:mwWord")
481 let g:mwWord = repeat([''], g:mwCycleMax)
483 if !exists("g:mwLastSearched")
484 let g:mwLastSearched = ""
487 call s:InitMarkVariables()
488 call mark#UpdateScope()