CTRLP
[my-vim-dotfolder.git] / autoload / mark.vim
blob9ebabb6861651b216ffc9bba8e5c76f572b5bdc4
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> 
10 " Dependencies:
11 "  - SearchSpecial.vim autoload script (optional, for improved search messages). 
13 " Version:     2.4.0
14 " Changes:
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
19 "   matches. 
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
33 "   patch. 
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
49 "     around its callers. 
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' )
63 endfunction
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]
68         if empty(l:regexp)
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) . '\>'
75                 elseif l:cword != ''
76                         let l:regexp = s:EscapeText(l:cword)
77                 endif
78         endif
80         if ! empty(l:regexp)
81                 call mark#DoMark(l:regexp)
82         endif
83 endfunction
85 function! s:GetVisualSelection()
86         let save_a = @a
87         silent normal! gv"ay
88         let res = @a
89         let @a = save_a
90         return res
91 endfunction
92 function! mark#GetVisualSelectionAsLiteralPattern()
93         return s:EscapeText(s:GetVisualSelection())
94 endfunction
95 function! mark#GetVisualSelectionAsRegexp()
96         return substitute(s:GetVisualSelection(), '\n', '', 'g')
97 endfunction
99 " Manually input a regular expression. 
100 function! mark#MarkRegex( regexpPreset )
101         call inputsave()
102         echohl Question
103         let l:regexp = input('Input pattern to mark: ', a:regexpPreset)
104         echohl None
105         call inputrestore()
106         if ! empty(l:regexp)
107                 call mark#DoMark(l:regexp)
108         endif
109 endfunction
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
116 endfunction
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
124                 endif
125         endfor
127         if ! empty(a:expr)
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)
135         endif
136 endfunction
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
149 endfunction
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
162 endfunction
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
168         if empty(regexp)
169                 let i = 0
170                 let indices = []
171                 while i < g:mwCycleMax
172                         if !empty(g:mwWord[i])
173                                 let g:mwWord[i] = ''
174                                 call add(indices, i)
175                         endif
176                         let i += 1
177                 endwhile
178                 let g:mwLastSearched = ""
179                 call s:MarkScope(l:indices, '')
180                 return
181         endif
183         " clear the mark if it has been marked
184         let i = 0
185         while i < g:mwCycleMax
186                 if regexp == g:mwWord[i]
187                         if g:mwLastSearched == g:mwWord[i]
188                                 let g:mwLastSearched = ''
189                         endif
190                         let g:mwWord[i] = ''
191                         call s:MarkScope([i], '')
192                         return
193                 endif
194                 let i += 1
195         endwhile
197         " add to history
198         if stridx(g:mwHistAdd, "/") >= 0
199                 call histadd("/", regexp)
200         endif
201         if stridx(g:mwHistAdd, "@") >= 0
202                 call histadd("@", regexp)
203         endif
205         " choose an unused mark group
206         let i = 0
207         while i < g:mwCycleMax
208                 if empty(g:mwWord[i])
209                         let g:mwWord[i] = regexp
210                         call s:Cycle(i)
211                         call s:MarkScope([i], regexp)
212                         return
213                 endif
214                 let i += 1
215         endwhile
217         " choose a mark group by cycle
218         let i = s:Cycle()
219         if g:mwLastSearched == g:mwWord[i]
220                 let g:mwLastSearched = ''
221         endif
222         let g:mwWord[i] = regexp
223         call s:MarkScope([i], regexp)
224 endfunction
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)
229         endif
231         let i = 0
232         while i < g:mwCycleMax
233                 if empty(g:mwWord[i])
234                         call s:MarkMatch([i], '')
235                 else
236                         call s:MarkMatch([i], g:mwWord[i])
237                 endif
238                 let i += 1
239         endwhile
240 endfunction
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(".")
246         let i = 0
247         while i < g:mwCycleMax
248                 if !empty(g:mwWord[i])
249                         " Note: col() is 1-based, all other indexes zero-based! 
250                         let start = 0
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)]]
256                                 endif
257                                 if b == e
258                                         break
259                                 endif
260                                 let start = e
261                         endwhile
262                 endif
263                 let i += 1
264         endwhile
265         return ['', []]
266 endfunction
268 " Search current mark. 
269 function! mark#SearchCurrentMark( isBackward )
270         let [l:markText, l:markPosition] = mark#CurrentMark()
271         if empty(l:markText)
272                 if empty(g:mwLastSearched)
273                         call mark#SearchAnyMark(a:isBackward)
274                         let g:mwLastSearched = mark#CurrentMark()[0]
275                 else
276                         call s:Search(g:mwLastSearched, a:isBackward, [], 'same-mark')
277                 endif
278         else
279                 call s:Search(l:markText, a:isBackward, l:markPosition, (l:markText ==# g:mwLastSearched ? 'same-mark' : 'new-mark'))
280                 let g:mwLastSearched = l:markText
281         endif
282 endfunction
284 silent! call SearchSpecial#DoesNotExist()       " Execute a function to force autoload.  
285 if exists('*SearchSpecial#WrapMessage')
286         function! s:WrapMessage( searchType, searchPattern, isBackward )
287                 redraw
288                 call SearchSpecial#WrapMessage(a:searchType, a:searchPattern, a:isBackward)
289         endfunction
290         function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
291                 call SearchSpecial#EchoSearchPattern(a:searchType, a:searchPattern, a:isBackward)
292         endfunction
293 else
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) ? "..." : "")
297         endfunction
298         function! s:WrapMessage( searchType, searchPattern, isBackward )
299                 redraw
300                 let v:warningmsg = printf('%s search hit %s, continuing at %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), (a:isBackward ? 'BOTTOM' : 'TOP'))
301                 echohl WarningMsg
302                 echo s:Trim(v:warningmsg)
303                 echohl None
304         endfunction
305         function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
306                 let l:message = (a:isBackward ? '?' : '/') .  a:searchPattern
307                 echohl SearchSpecialSearchType
308                 echo a:searchType
309                 echohl None
310                 echon s:Trim(l:message)
311         endfunction
312 endif
313 function! s:ErrorMessage( searchType, searchPattern, isBackward )
314         if &wrapscan
315                 let v:errmsg = a:searchType . ' not found: ' . a:searchPattern
316         else
317                 let v:errmsg = printf('%s search hit %s without match for: %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), a:searchPattern)
318         endif
319         echohl ErrorMsg
320         echomsg v:errmsg
321         echohl None
322 endfunction
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
335         set nosmartcase
337         let l:count = v:count1
338         let [l:startLine, l:startCol] = [line('.'), col('.')]
339         let l:isWrapped = 0
340         let l:isMatch = 0
341         let l:line = 0
342         while l:count > 0
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
358                         " the current match. 
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. 
365                         if l:isMatch
366                                 let l:line = 0
367                                 break
368                         endif
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. 
373                         let l:isMatch = 1
374                 elseif l:line > 0
375                         let l:isMatch = 1
376                         let l:count -= 1
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)
381                                 let l:isWrapped = 1
382                         elseif a:isBackward && (l:startLine < l:line || l:startLine == l:line && l:startCol <= l:col)
383                                 let l:isWrapped = 1
384                         endif
385                 else
386                         break
387                 endif
388         endwhile
389         let &smartcase = l:save_smartcase
390         
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. 
398                 normal! zv
400                 " Add the original cursor position to the jump list, like the
401                 " [/?*#nN] commands. 
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)
407                 normal! m'
408                 call setpos('.', l:matchPosition)
410                 if l:isWrapped
411                         call s:WrapMessage(a:searchType, a:pattern, a:isBackward)
412                 else
413                         call s:EchoSearchPattern(a:searchType, a:pattern, a:isBackward)
414                 endif
415                 return 1
416         else
417                 if l:isMatch
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)
423                 endif
424                 call s:ErrorMessage(a:searchType, a:pattern, a:isBackward)
425                 return 0
426         endif
427 endfunction
429 " Combine all marks into one regexp. 
430 function! s:AnyMark()
431         return join(filter(copy(g:mwWord), '! empty(v:val)'), '\|')
432 endfunction
434 " Search any mark. 
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 = ""
440 endfunction
442 " Search last searched mark. 
443 function! mark#SearchNext( isBackward )
444         let l:markText = mark#CurrentMark()[0]
445         if empty(l:markText)
446                 return 0
447         else
448                 if empty(g:mwLastSearched)
449                         call mark#SearchAnyMark(a:isBackward)
450                 else
451                         call mark#SearchCurrentMark(a:isBackward)
452                 endif
453                 return 1
454         endif
455 endfunction
457 "- initializations ------------------------------------------------------------
458 augroup Mark
459         autocmd!
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()
463 augroup END
465 " Define global variables and initialize current scope.  
466 function! s:InitMarkVariables()
467         if !exists("g:mwHistAdd")
468                 let g:mwHistAdd = "/@"
469         endif
470         if !exists("g:mwCycleMax")
471                 let i = 1
472                 while hlexists("MarkWord" . i)
473                         let i = i + 1
474                 endwhile
475                 let g:mwCycleMax = i - 1
476         endif
477         if !exists("g:mwCycle")
478                 let g:mwCycle = 0
479         endif
480         if !exists("g:mwWord")
481                 let g:mwWord = repeat([''], g:mwCycleMax)
482         endif
483         if !exists("g:mwLastSearched")
484                 let g:mwLastSearched = ""
485         endif
486 endfunction
487 call s:InitMarkVariables()
488 call mark#UpdateScope()
490 " vim: ts=2 sw=2