blob: fe9e6ebaf44a6196a58789c803b9acf9d1adf602 [file] [log] [blame]
chatsiri rattanaf3c94332012-04-23 22:12:51 +07001"=============================================================================
2" Copyright (c) 2007-2010 Takeshi NISHIDA
3"
4"=============================================================================
5" LOAD GUARD {{{1
6
7if !l9#guardScriptLoading(expand('<sfile>:p'), 0, 0, [])
8 finish
9endif
10
11" }}}1
12"=============================================================================
13" GLOBAL FUNCTIONS {{{1
14
15
16" returns list of paths.
17" An argument for glob() is normalized in order to avoid a bug on Windows.
18function fuf#glob(expr)
19 " Substitutes "\", because on Windows, "**\" doesn't include ".\",
20 " but "**/" include "./". I don't know why.
21 return split(glob(substitute(a:expr, '\', '/', 'g')), "\n")
22endfunction
23
24"
25function fuf#countModifiedFiles(files, time)
26 return len(filter(copy(a:files), 'getftime(expand(v:val)) > a:time'))
27endfunction
28
29"
30function fuf#getCurrentTagFiles()
31 return sort(filter(map(tagfiles(), 'fnamemodify(v:val, '':p'')'), 'filereadable(v:val)'))
32endfunction
33
34"
35function fuf#mapToSetSerialIndex(in, offset)
36 for i in range(len(a:in))
37 let a:in[i].index = i + a:offset
38 endfor
39 return a:in
40endfunction
41
42"
43function fuf#updateMruList(mrulist, newItem, maxItem, exclude)
44 let result = copy(a:mrulist)
45 let result = filter(result,'v:val.word !=# a:newItem.word')
46 let result = insert(result, a:newItem)
47 if len(a:exclude)
48 let result = filter(result, 'v:val.word !~ a:exclude')
49 endif
50 return result[0 : a:maxItem - 1]
51endfunction
52
53" takes suffix number. if no digits, returns -1
54function fuf#suffixNumber(str)
55 let s = matchstr(a:str, '\d\+$')
56 return (len(s) ? str2nr(s) : -1)
57endfunction
58
59" "foo/bar/buz/hoge" -> { head: "foo/bar/buz/", tail: "hoge" }
60function fuf#splitPath(path)
61 let head = matchstr(a:path, '^.*[/\\]')
62 return {
63 \ 'head' : head,
64 \ 'tail' : a:path[strlen(head):]
65 \ }
66endfunction
67
68" "foo/.../bar/...hoge" -> "foo/.../bar/../../hoge"
69function fuf#expandTailDotSequenceToParentDir(pattern)
70 return substitute(a:pattern, '^\(.*[/\\]\)\?\zs\.\(\.\+\)\ze[^/\\]*$',
71 \ '\=repeat(".." . l9#getPathSeparator(), len(submatch(2)))', '')
72endfunction
73
74"
75function fuf#formatPrompt(prompt, partialMatching, otherString)
76 let indicator = escape((a:partialMatching ? '!' : '') . a:otherString, '\')
77 return substitute(a:prompt, '[]', indicator, 'g')
78endfunction
79
80"
81function fuf#getFileLines(file)
82 let bufnr = (type(a:file) ==# type(0) ? a:file : bufnr('^' . a:file . '$'))
83 let lines = getbufline(bufnr, 1, '$')
84 if !empty(lines)
85 return lines
86 endif
87 return l9#readFile(a:file)
88endfunction
89
90"
91function fuf#makePreviewLinesAround(lines, indices, page, maxHeight)
92 let index = ((empty(a:indices) ? 0 : a:indices[0])
93 \ + a:page * a:maxHeight) % len(a:lines)
94 if empty(a:lines) || a:maxHeight <= 0
95 return []
96 endif
97 let beg = max([0, index - a:maxHeight / 2])
98 let end = min([beg + a:maxHeight, len(a:lines)])
99 let beg = max([0, end - a:maxHeight])
100 let lines = []
101 for i in range(beg, end - 1)
102 let mark = (count(a:indices, i) ? '>' : ' ')
103 call add(lines, printf('%s%4d ', mark, i + 1) . a:lines[i])
104 endfor
105 return lines
106endfunction
107
108" a:file: a path string or a buffer number
109function fuf#makePreviewLinesForFile(file, count, maxHeight)
110 let lines = fuf#getFileLines(a:file)
111 if empty(lines)
112 return []
113 endif
114 let bufnr = (type(a:file) ==# type(0) ? a:file : bufnr('^' . a:file . '$'))
115 if exists('s:bufferCursorPosMap[bufnr]')
116 let indices = [s:bufferCursorPosMap[bufnr][1] - 1]
117 else
118 let indices = []
119 endif
120 return fuf#makePreviewLinesAround(
121 \ lines, indices, a:count, a:maxHeight)
122endfunction
123
124"
125function fuf#echoWarning(msg)
126 call l9#echoHl('WarningMsg', a:msg, '[fuf] ', 1)
127endfunction
128
129"
130function fuf#echoError(msg)
131 call l9#echoHl('ErrorMsg', a:msg, '[fuf] ', 1)
132endfunction
133
134"
135function fuf#openBuffer(bufNr, mode, reuse)
136 if a:reuse && ((a:mode ==# s:OPEN_TYPE_SPLIT &&
137 \ l9#moveToBufferWindowInCurrentTabpage(a:bufNr)) ||
138 \ (a:mode ==# s:OPEN_TYPE_VSPLIT &&
139 \ l9#moveToBufferWindowInCurrentTabpage(a:bufNr)) ||
140 \ (a:mode ==# s:OPEN_TYPE_TAB &&
141 \ l9#moveToBufferWindowInOtherTabpage(a:bufNr)))
142 return
143 endif
144 execute printf({
145 \ s:OPEN_TYPE_CURRENT : '%sbuffer' ,
146 \ s:OPEN_TYPE_SPLIT : '%ssbuffer' ,
147 \ s:OPEN_TYPE_VSPLIT : 'vertical %ssbuffer',
148 \ s:OPEN_TYPE_TAB : 'tab %ssbuffer' ,
149 \ }[a:mode], a:bufNr)
150endfunction
151
152"
153function fuf#openFile(path, mode, reuse)
154 let bufNr = bufnr('^' . a:path . '$')
155 if bufNr > -1
156 call fuf#openBuffer(bufNr, a:mode, a:reuse)
157 else
158 execute {
159 \ s:OPEN_TYPE_CURRENT : 'edit ' ,
160 \ s:OPEN_TYPE_SPLIT : 'split ' ,
161 \ s:OPEN_TYPE_VSPLIT : 'vsplit ' ,
162 \ s:OPEN_TYPE_TAB : 'tabedit ',
163 \ }[a:mode] . fnameescape(fnamemodify(a:path, ':~:.'))
164 endif
165endfunction
166
167"
168function fuf#openTag(tag, mode)
169 execute {
170 \ s:OPEN_TYPE_CURRENT : 'tjump ' ,
171 \ s:OPEN_TYPE_SPLIT : 'stjump ' ,
172 \ s:OPEN_TYPE_VSPLIT : 'vertical stjump ',
173 \ s:OPEN_TYPE_TAB : 'tab stjump ' ,
174 \ }[a:mode] . a:tag
175endfunction
176
177"
178function fuf#openHelp(tag, mode)
179 execute {
180 \ s:OPEN_TYPE_CURRENT : 'help ' ,
181 \ s:OPEN_TYPE_SPLIT : 'help ' ,
182 \ s:OPEN_TYPE_VSPLIT : 'vertical help ',
183 \ s:OPEN_TYPE_TAB : 'tab help ' ,
184 \ }[a:mode] . a:tag
185endfunction
186
187"
188function fuf#prejump(mode)
189 execute {
190 \ s:OPEN_TYPE_CURRENT : '' ,
191 \ s:OPEN_TYPE_SPLIT : 'split' ,
192 \ s:OPEN_TYPE_VSPLIT : 'vsplit' ,
193 \ s:OPEN_TYPE_TAB : 'tab split',
194 \ }[a:mode]
195endfunction
196
197"
198function fuf#compareRanks(i1, i2)
199 if exists('a:i1.ranks') && exists('a:i2.ranks')
200 for i in range(min([len(a:i1.ranks), len(a:i2.ranks)]))
201 if a:i1.ranks[i] > a:i2.ranks[i]
202 return +1
203 elseif a:i1.ranks[i] < a:i2.ranks[i]
204 return -1
205 endif
206 endfor
207 endif
208 return 0
209endfunction
210
211"
212function fuf#makePathItem(fname, menu, appendsDirSuffix)
213 let pathPair = fuf#splitPath(a:fname)
214 let dirSuffix = (a:appendsDirSuffix && isdirectory(expand(a:fname))
215 \ ? l9#getPathSeparator()
216 \ : '')
217 return {
218 \ 'word' : a:fname . dirSuffix,
219 \ 'wordForPrimaryHead': s:toLowerForIgnoringCase(pathPair.head),
220 \ 'wordForPrimaryTail': s:toLowerForIgnoringCase(pathPair.tail),
221 \ 'wordForBoundary' : s:toLowerForIgnoringCase(s:getWordBoundaries(pathPair.tail)),
222 \ 'wordForRefining' : s:toLowerForIgnoringCase(a:fname . dirSuffix),
223 \ 'wordForRank' : s:toLowerForIgnoringCase(pathPair.tail),
224 \ 'menu' : a:menu,
225 \ }
226endfunction
227
228"
229function fuf#makeNonPathItem(word, menu)
230 let wordL = s:toLowerForIgnoringCase(a:word)
231 return {
232 \ 'word' : a:word,
233 \ 'wordForPrimary' : wordL,
234 \ 'wordForBoundary': s:toLowerForIgnoringCase(s:getWordBoundaries(a:word)),
235 \ 'wordForRefining': wordL,
236 \ 'wordForRank' : wordL,
237 \ 'menu' : a:menu,
238 \ }
239endfunction
240
241"
242function fuf#makePatternSet(patternBase, interpreter, partialMatching)
243 let MakeMatchingExpr = function(a:partialMatching
244 \ ? 's:makePartialMatchingExpr'
245 \ : 's:makeFuzzyMatchingExpr')
246 let [primary; refinings] = split(a:patternBase, g:fuf_patternSeparator, 1)
247 let elements = call(a:interpreter, [primary])
248 let primaryExprs = map(elements.matchingPairs, 'MakeMatchingExpr(v:val[0], v:val[1])')
249 let refiningExprs = map(refinings, 's:makeRefiningExpr(v:val)')
250 return {
251 \ 'primary' : elements.primary,
252 \ 'primaryForRank': elements.primaryForRank,
253 \ 'filteringExpr' : join(primaryExprs + refiningExprs, ' && '),
254 \ }
255endfunction
256
257"
258function fuf#enumExpandedDirsEntries(dir, exclude)
259 let entries = fuf#glob(a:dir . '*') + fuf#glob(a:dir . '.*')
260 " removes "*/." and "*/.."
261 call filter(entries, 'v:val !~ ''\v(^|[/\\])\.\.?$''')
262 call map(entries, 'fuf#makePathItem(v:val, "", 1)')
263 if len(a:exclude)
264 call filter(entries, 'v:val.word !~ a:exclude')
265 endif
266 return entries
267endfunction
268
269"
270function fuf#mapToSetAbbrWithSnippedWordAsPath(items)
271 let maxLenStats = {}
272 call map(a:items, 's:makeFileAbbrInfo(v:val, maxLenStats)')
273 let snippedHeads =
274 \ map(maxLenStats, 's:getSnippedHead(v:key[: -2], v:val)')
275 return map(a:items, 's:setAbbrWithFileAbbrData(v:val, snippedHeads)')
276endfunction
277
278"
279function fuf#setAbbrWithFormattedWord(item, abbrIndex)
280 let lenMenu = (exists('a:item.menu') ? len(a:item.menu) + 2 : 0)
281 let abbrPrefix = (exists('a:item.abbrPrefix') ? a:item.abbrPrefix : '')
282 let a:item.abbr = abbrPrefix . a:item.word
283 if a:abbrIndex
284 let a:item.abbr = printf('%4d: ', a:item.index) . a:item.abbr
285 endif
286 let a:item.abbr = l9#snipTail(a:item.abbr, g:fuf_maxMenuWidth - lenMenu, s:ABBR_SNIP_MASK)
287 return a:item
288endfunction
289
290"
291function s:onCommandPre()
292 for m in filter(copy(fuf#getModeNames()), 'fuf#{v:val}#requiresOnCommandPre()')
293 call fuf#{m}#onCommandPre(getcmdtype() . getcmdline())
294 endfor
295 " lets last entry become the newest in the history
296 call histadd(getcmdtype(), getcmdline())
297 " this is not mapped again (:help recursive_mapping)
298 return "\<CR>"
299endfunction
300
301"
302let s:modeNames = []
303
304"
305function fuf#addMode(modeName)
306 if count(g:fuf_modesDisable, a:modeName) > 0
307 return
308 endif
309 call add(s:modeNames, a:modeName)
310 call fuf#{a:modeName}#renewCache()
311 call fuf#{a:modeName}#onInit()
312 if fuf#{a:modeName}#requiresOnCommandPre()
313 " cnoremap has a problem, which doesn't expand cabbrev.
314 cmap <silent> <expr> <CR> <SID>onCommandPre()
315 endif
316endfunction
317
318"
319function fuf#getModeNames()
320 return s:modeNames
321endfunction
322
323"
324function fuf#defineLaunchCommand(CmdName, modeName, prefixInitialPattern, tempVars)
325 if empty(a:tempVars)
326 let preCmd = ''
327 else
328 let preCmd = printf('call l9#tempvariables#setList(%s, %s) | ',
329 \ string(s:TEMP_VARIABLES_GROUP), string(a:tempVars))
330 endif
331 execute printf('command! -range -bang -narg=? %s %s call fuf#launch(%s, %s . <q-args>, len(<q-bang>))',
332 \ a:CmdName, preCmd, string(a:modeName), a:prefixInitialPattern)
333endfunction
334
335"
336function fuf#defineKeyMappingInHandler(key, func)
337 " hacks to be able to use feedkeys().
338 execute printf(
339 \ 'inoremap <buffer> <silent> %s <C-r>=fuf#getRunningHandler().%s ? "" : ""<CR>',
340 \ a:key, a:func)
341endfunction
342
343"
344let s:oneTimeVariables = []
345
346"
347function fuf#setOneTimeVariables(...)
348 let s:oneTimeVariables += a:000
349endfunction
350
351"
352function fuf#launch(modeName, initialPattern, partialMatching)
353 if exists('s:runningHandler')
354 call fuf#echoWarning('FuzzyFinder is running.')
355 endif
356 if count(fuf#getModeNames(), a:modeName) == 0
357 echoerr 'This mode is not available: ' . a:modeName
358 return
359 endif
360 let s:runningHandler = fuf#{a:modeName}#createHandler(copy(s:handlerBase))
361 let s:runningHandler.stats = fuf#loadDataFile(s:runningHandler.getModeName(), 'stats')
362 let s:runningHandler.partialMatching = a:partialMatching
363 let s:runningHandler.bufNrPrev = bufnr('%')
364 let s:runningHandler.lastCol = -1
365 let s:runningHandler.windowRestoringCommand = winrestcmd()
366 call s:runningHandler.onModeEnterPre()
367 " NOTE: updatetime is set, because in Buffer-Tag mode on Vim 7.3 on Windows,
368 " Vim keeps from triggering CursorMovedI for updatetime after system() is
369 " called. I don't know why.
370 call fuf#setOneTimeVariables(
371 \ ['&completeopt', 'menuone'],
372 \ ['&ignorecase', 0],
373 \ ['&updatetime', 10],
374 \ )
375 if s:runningHandler.getPreviewHeight() > 0
376 call fuf#setOneTimeVariables(
377 \ ['&cmdheight', s:runningHandler.getPreviewHeight() + 1])
378 endif
379 call l9#tempvariables#setList(s:TEMP_VARIABLES_GROUP, s:oneTimeVariables)
380 let s:oneTimeVariables = []
381 call s:activateFufBuffer()
382 augroup FufLocal
383 autocmd!
384 autocmd CursorMovedI <buffer> call s:runningHandler.onCursorMovedI()
385 autocmd InsertLeave <buffer> nested call s:runningHandler.onInsertLeave()
386 augroup END
387 for [key, func] in [
388 \ [ g:fuf_keyOpen , 'onCr(' . s:OPEN_TYPE_CURRENT . ')' ],
389 \ [ g:fuf_keyOpenSplit , 'onCr(' . s:OPEN_TYPE_SPLIT . ')' ],
390 \ [ g:fuf_keyOpenVsplit , 'onCr(' . s:OPEN_TYPE_VSPLIT . ')' ],
391 \ [ g:fuf_keyOpenTabpage , 'onCr(' . s:OPEN_TYPE_TAB . ')' ],
392 \ [ '<BS>' , 'onBs()' ],
393 \ [ '<C-h>' , 'onBs()' ],
394 \ [ '<C-w>' , 'onDeleteWord()' ],
395 \ [ g:fuf_keyPreview , 'onPreviewBase(1)' ],
396 \ [ g:fuf_keyNextMode , 'onSwitchMode(+1)' ],
397 \ [ g:fuf_keyPrevMode , 'onSwitchMode(-1)' ],
398 \ [ g:fuf_keySwitchMatching, 'onSwitchMatching()' ],
399 \ [ g:fuf_keyPrevPattern , 'onRecallPattern(+1)' ],
400 \ [ g:fuf_keyNextPattern , 'onRecallPattern(-1)' ],
401 \ ]
402 call fuf#defineKeyMappingInHandler(key, func)
403 endfor
404 " Starts Insert mode and makes CursorMovedI event now. Command prompt is
405 " needed to forces a completion menu to update every typing.
406 call setline(1, s:runningHandler.getPrompt() . a:initialPattern)
407 call s:runningHandler.onModeEnterPost()
408 call feedkeys("A", 'n') " startinsert! does not work in InsertLeave event handler
409 redraw
410endfunction
411
412"
413function fuf#loadDataFile(modeName, dataName)
414 if !s:dataFileAvailable
415 return []
416 endif
417 let lines = l9#readFile(l9#concatPaths([g:fuf_dataDir, a:modeName, a:dataName]))
418 return map(lines, 'eval(v:val)')
419endfunction
420
421"
422function fuf#saveDataFile(modeName, dataName, items)
423 if !s:dataFileAvailable
424 return -1
425 endif
426 let lines = map(copy(a:items), 'string(v:val)')
427 return l9#writeFile(lines, l9#concatPaths([g:fuf_dataDir, a:modeName, a:dataName]))
428endfunction
429
430"
431function fuf#getDataFileTime(modeName, dataName)
432 if !s:dataFileAvailable
433 return -1
434 endif
435 return getftime(expand(l9#concatPaths([g:fuf_dataDir, a:modeName, a:dataName])))
436endfunction
437
438"
439function s:createDataBufferListener(dataFile)
440 let listener = { 'dataFile': a:dataFile }
441
442 function listener.onWrite(lines)
443 let [modeName, dataName] = split(self.dataFile, l9#getPathSeparator())
444 let items = map(filter(a:lines, '!empty(v:val)'), 'eval(v:val)')
445 call fuf#saveDataFile(modeName, dataName, items)
446 echo "Data files updated"
447 return 1
448 endfunction
449
450 return listener
451endfunction
452
453"
454function s:createEditDataListener()
455 let listener = {}
456
457 function listener.onComplete(dataFile, method)
458 let bufName = '[fuf-info]'
459 let lines = l9#readFile(l9#concatPaths([g:fuf_dataDir, a:dataFile]))
460 call l9#tempbuffer#openWritable(bufName, 'vim', lines, 0, 0, 0,
461 \ s:createDataBufferListener(a:dataFile))
462 endfunction
463
464 return listener
465endfunction
466
467"
468function s:getEditableDataFiles(modeName)
469 let dataFiles = fuf#{a:modeName}#getEditableDataNames()
470 call filter(dataFiles, 'fuf#getDataFileTime(a:modeName, v:val) != -1')
471 return map(dataFiles, 'l9#concatPaths([a:modeName, v:val])')
472endfunction
473
474"
475function fuf#editDataFile()
476 let dataFiles = map(copy(fuf#getModeNames()), 's:getEditableDataFiles(v:val)')
477 let dataFiles = l9#concat(dataFiles)
478 call fuf#callbackitem#launch('', 0, '>Mode>', s:createEditDataListener(), dataFiles, 0)
479endfunction
480
481"
482function fuf#getRunningHandler()
483 return s:runningHandler
484endfunction
485
486"
487function fuf#onComplete(findstart, base)
488 return s:runningHandler.onComplete(a:findstart, a:base)
489endfunction
490
491" }}}1
492"=============================================================================
493" LOCAL FUNCTIONS/VARIABLES {{{1
494
495let s:TEMP_VARIABLES_GROUP = expand('<sfile>:p')
496let s:ABBR_SNIP_MASK = '...'
497let s:OPEN_TYPE_CURRENT = 1
498let s:OPEN_TYPE_SPLIT = 2
499let s:OPEN_TYPE_VSPLIT = 3
500let s:OPEN_TYPE_TAB = 4
501
502" a:pattern: 'str' -> '\V\.\*s\.\*t\.\*r\.\*'
503function s:makeFuzzyMatchingExpr(target, pattern)
504 let wi = ''
505 for c in split(a:pattern, '\zs')
506 if wi =~# '[^*?]$' && c !~ '[*?]'
507 let wi .= '*'
508 endif
509 let wi .= c
510 endfor
511 return s:makePartialMatchingExpr(a:target, wi)
512endfunction
513
514" a:pattern: 'str' -> '\Vstr'
515" 'st*r' -> '\Vst\.\*r'
516function s:makePartialMatchingExpr(target, pattern)
517 let patternMigemo = s:makeAdditionalMigemoPattern(a:pattern)
518 if a:pattern !~ '[*?]' && empty(patternMigemo)
519 " NOTE: stridx is faster than regexp matching
520 return 'stridx(' . a:target . ', ' . string(a:pattern) . ') >= 0'
521 endif
522 return a:target . ' =~# ' .
523 \ string(l9#convertWildcardToRegexp(a:pattern)) . patternMigemo
524endfunction
525
526"
527function s:makeRefiningExpr(pattern)
528 if g:fuf_fuzzyRefining
529 let expr = s:makeFuzzyMatchingExpr('v:val.wordForRefining', a:pattern)
530 else
531 let expr = s:makePartialMatchingExpr('v:val.wordForRefining', a:pattern)
532 endif
533 if a:pattern =~# '\D'
534 return expr
535 else
536 return '(' . expr . ' || v:val.index == ' . string(a:pattern) . ')'
537 endif
538endfunction
539
540"
541function s:makeAdditionalMigemoPattern(pattern)
542 if !g:fuf_useMigemo || a:pattern =~# '[^\x01-\x7e]'
543 return ''
544 endif
545 return '\|\m' . substitute(migemo(a:pattern), '\\_s\*', '.*', 'g')
546endfunction
547
548"
549function s:interpretPrimaryPatternForPathTail(pattern)
550 let pattern = fuf#expandTailDotSequenceToParentDir(a:pattern)
551 let pairL = fuf#splitPath(s:toLowerForIgnoringCase(pattern))
552 return {
553 \ 'primary' : pattern,
554 \ 'primaryForRank': pairL.tail,
555 \ 'matchingPairs' : [['v:val.wordForPrimaryTail', pairL.tail],],
556 \ }
557endfunction
558
559"
560function s:interpretPrimaryPatternForPath(pattern)
561 let pattern = fuf#expandTailDotSequenceToParentDir(a:pattern)
562 let patternL = s:toLowerForIgnoringCase(pattern)
563 let pairL = fuf#splitPath(patternL)
564 if g:fuf_splitPathMatching
565 let matches = [
566 \ ['v:val.wordForPrimaryHead', pairL.head],
567 \ ['v:val.wordForPrimaryTail', pairL.tail],
568 \ ]
569 else
570 let matches = [
571 \ ['v:val.wordForPrimaryHead . v:val.wordForPrimaryTail', patternL],
572 \ ]
573 endif
574 return {
575 \ 'primary' : pattern,
576 \ 'primaryForRank': pairL.tail,
577 \ 'matchingPairs' : matches,
578 \ }
579endfunction
580
581"
582function s:interpretPrimaryPatternForNonPath(pattern)
583 let patternL = s:toLowerForIgnoringCase(a:pattern)
584 return {
585 \ 'primary' : a:pattern,
586 \ 'primaryForRank': patternL,
587 \ 'matchingPairs' : [['v:val.wordForPrimary', patternL],],
588 \ }
589endfunction
590
591"
592function s:getWordBoundaries(word)
593 return substitute(a:word, '\a\zs\l\+\|\zs\A', '', 'g')
594endfunction
595
596"
597function s:toLowerForIgnoringCase(str)
598 return (g:fuf_ignoreCase ? tolower(a:str) : a:str)
599endfunction
600
601"
602function s:setRanks(item, pattern, exprBoundary, stats)
603 "let word2 = substitute(a:eval_word, '\a\zs\l\+\|\zs\A', '', 'g')
604 let a:item.ranks = [
605 \ s:evaluateLearningRank(a:item.word, a:stats),
606 \ -s:scoreSequentialMatching(a:item.wordForRank, a:pattern),
607 \ -s:scoreBoundaryMatching(a:item.wordForBoundary,
608 \ a:pattern, a:exprBoundary),
609 \ a:item.index,
610 \ ]
611 return a:item
612endfunction
613
614"
615function s:evaluateLearningRank(word, stats)
616 for i in range(len(a:stats))
617 if a:stats[i].word ==# a:word
618 return i
619 endif
620 endfor
621 return len(a:stats)
622endfunction
623
624" range of return value is [0.0, 1.0]
625function s:scoreSequentialMatching(word, pattern)
626 if empty(a:pattern)
627 return str2float('0.0')
628 endif
629 let pos = stridx(a:word, a:pattern)
630 if pos < 0
631 return str2float('0.0')
632 endif
633 let lenRest = len(a:word) - len(a:pattern) - pos
634 return str2float(pos == 0 ? '0.5' : '0.0') + str2float('0.5') / (lenRest + 1)
635endfunction
636
637" range of return value is [0.0, 1.0]
638function s:scoreBoundaryMatching(wordForBoundary, pattern, exprBoundary)
639 if empty(a:pattern)
640 return str2float('0.0')
641 endif
642 if !eval(a:exprBoundary)
643 return 0
644 endif
645 return (s:scoreSequentialMatching(a:wordForBoundary, a:pattern) + 1) / 2
646endfunction
647
648"
649function s:highlightPrompt(prompt)
650 syntax clear
651 execute printf('syntax match %s /^\V%s/', g:fuf_promptHighlight, escape(a:prompt, '\/'))
652endfunction
653
654"
655function s:highlightError()
656 syntax clear
657 syntax match Error /^.*$/
658endfunction
659
660"
661function s:expandAbbrevMap(pattern, abbrevMap)
662 let result = [a:pattern]
663 for [pattern, subs] in items(a:abbrevMap)
664 let exprs = result
665 let result = []
666 for expr in exprs
667 let result += map(copy(subs), 'substitute(expr, pattern, escape(v:val, ''\''), "g")')
668 endfor
669 endfor
670 return l9#unique(result)
671endfunction
672
673"
674function s:makeFileAbbrInfo(item, maxLenStats)
675 let head = matchstr(a:item.word, '^.*[/\\]\ze.')
676 let a:item.abbr = { 'head' : head,
677 \ 'tail' : a:item.word[strlen(head):],
678 \ 'key' : head . '.',
679 \ 'prefix' : printf('%4d: ', a:item.index), }
680 if exists('a:item.abbrPrefix')
681 let a:item.abbr.prefix .= a:item.abbrPrefix
682 endif
683 let len = len(a:item.abbr.prefix) + len(a:item.word) +
684 \ (exists('a:item.menu') ? len(a:item.menu) + 2 : 0)
685 if !exists('a:maxLenStats[a:item.abbr.key]') || len > a:maxLenStats[a:item.abbr.key]
686 let a:maxLenStats[a:item.abbr.key] = len
687 endif
688 return a:item
689endfunction
690
691"
692function s:getSnippedHead(head, baseLen)
693 return l9#snipMid(a:head, len(a:head) + g:fuf_maxMenuWidth - a:baseLen, s:ABBR_SNIP_MASK)
694endfunction
695
696"
697function s:setAbbrWithFileAbbrData(item, snippedHeads)
698 let lenMenu = (exists('a:item.menu') ? len(a:item.menu) + 2 : 0)
699 let abbr = a:item.abbr.prefix . a:snippedHeads[a:item.abbr.key] . a:item.abbr.tail
700 let a:item.abbr = l9#snipTail(abbr, g:fuf_maxMenuWidth - lenMenu, s:ABBR_SNIP_MASK)
701 return a:item
702endfunction
703
704"
705let s:FUF_BUF_NAME = '[fuf]'
706
707"
708function s:activateFufBuffer()
709 " lcd . : To avoid the strange behavior that unnamed buffer changes its cwd
710 " if 'autochdir' was set on.
711 lcd .
712 let cwd = getcwd()
713 call l9#tempbuffer#openScratch(s:FUF_BUF_NAME, 'fuf', [], 1, 0, 1, {})
714 resize 1 " for issue #21
715 " lcd ... : countermeasure against auto-cd script
716 lcd `=cwd`
717 setlocal nocursorline " for highlighting
718 setlocal nocursorcolumn " for highlighting
719 setlocal omnifunc=fuf#onComplete
720 redraw " for 'lazyredraw'
721 if exists(':AcpLock')
722 AcpLock
723 elseif exists(':AutoComplPopLock')
724 AutoComplPopLock
725 endif
726endfunction
727
728"
729function s:deactivateFufBuffer()
730 if exists(':AcpUnlock')
731 AcpUnlock
732 elseif exists(':AutoComplPopUnlock')
733 AutoComplPopUnlock
734 endif
735 call l9#tempbuffer#close(s:FUF_BUF_NAME)
736endfunction
737
738" }}}1
739"=============================================================================
740" s:handlerBase {{{1
741
742let s:handlerBase = {}
743
744"-----------------------------------------------------------------------------
745" PURE VIRTUAL FUNCTIONS {{{2
746"
747" "
748" s:handler.getModeName()
749"
750" "
751" s:handler.getPrompt()
752"
753" "
754" s:handler.getCompleteItems(patternSet)
755"
756" "
757" s:handler.onOpen(word, mode)
758"
759" " Before entering FuzzyFinder buffer. This function should return in a short time.
760" s:handler.onModeEnterPre()
761"
762" " After entering FuzzyFinder buffer.
763" s:handler.onModeEnterPost()
764"
765" " After leaving FuzzyFinder buffer.
766" s:handler.onModeLeavePost(opened)
767"
768" }}}2
769"-----------------------------------------------------------------------------
770
771"
772function s:handlerBase.concretize(deriv)
773 call extend(self, a:deriv, 'error')
774 return self
775endfunction
776
777"
778function s:handlerBase.addStat(pattern, word)
779 let stat = { 'pattern' : a:pattern, 'word' : a:word }
780 call filter(self.stats, 'v:val !=# stat')
781 call insert(self.stats, stat)
782 let self.stats = self.stats[0 : g:fuf_learningLimit - 1]
783endfunction
784
785"
786function s:handlerBase.getMatchingCompleteItems(patternBase)
787 let MakeMatchingExpr = function(self.partialMatching
788 \ ? 's:makePartialMatchingExpr'
789 \ : 's:makeFuzzyMatchingExpr')
790 let patternSet = self.makePatternSet(a:patternBase)
791 let exprBoundary = s:makeFuzzyMatchingExpr('a:wordForBoundary', patternSet.primaryForRank)
792 let stats = filter(
793 \ copy(self.stats), 'v:val.pattern ==# patternSet.primaryForRank')
794 let items = self.getCompleteItems(patternSet.primary)
795 " NOTE: In order to know an excess, plus 1 to limit number
796 let items = l9#filterWithLimit(
797 \ items, patternSet.filteringExpr, g:fuf_enumeratingLimit + 1)
798 return map(items,
799 \ 's:setRanks(v:val, patternSet.primaryForRank, exprBoundary, stats)')
800endfunction
801
802"
803function s:handlerBase.onComplete(findstart, base)
804 if a:findstart
805 return 0
806 elseif !self.existsPrompt(a:base)
807 return []
808 endif
809 call s:highlightPrompt(self.getPrompt())
810 let items = []
811 for patternBase in s:expandAbbrevMap(self.removePrompt(a:base), g:fuf_abbrevMap)
812 let items += self.getMatchingCompleteItems(patternBase)
813 if len(items) > g:fuf_enumeratingLimit
814 let items = items[ : g:fuf_enumeratingLimit - 1]
815 call s:highlightError()
816 break
817 endif
818 endfor
819 if empty(items)
820 call s:highlightError()
821 else
822 call sort(items, 'fuf#compareRanks')
823 if g:fuf_autoPreview
824 call feedkeys("\<C-p>\<Down>\<C-r>=fuf#getRunningHandler().onPreviewBase(0) ? '' : ''\<CR>", 'n')
825 else
826 call feedkeys("\<C-p>\<Down>", 'n')
827 endif
828 let self.lastFirstWord = items[0].word
829 endif
830 return items
831endfunction
832
833"
834function s:handlerBase.existsPrompt(line)
835 return strlen(a:line) >= strlen(self.getPrompt()) &&
836 \ a:line[:strlen(self.getPrompt()) -1] ==# self.getPrompt()
837endfunction
838
839"
840function s:handlerBase.removePrompt(line)
841 return a:line[(self.existsPrompt(a:line) ? strlen(self.getPrompt()) : 0):]
842endfunction
843
844"
845function s:handlerBase.restorePrompt(line)
846 let i = 0
847 while i < len(self.getPrompt()) && i < len(a:line) && self.getPrompt()[i] ==# a:line[i]
848 let i += 1
849 endwhile
850 return self.getPrompt() . a:line[i : ]
851endfunction
852
853"
854function s:handlerBase.onCursorMovedI()
855 if !self.existsPrompt(getline('.'))
856 call setline('.', self.restorePrompt(getline('.')))
857 call feedkeys("\<End>", 'n')
858 elseif col('.') <= len(self.getPrompt())
859 " if the cursor is moved before command prompt
860 call feedkeys(repeat("\<Right>", len(self.getPrompt()) - col('.') + 1), 'n')
861 elseif col('.') > strlen(getline('.')) && col('.') != self.lastCol
862 " if the cursor is placed on the end of the line and has been actually moved.
863 let self.lastCol = col('.')
864 let self.lastPattern = self.removePrompt(getline('.'))
865 call feedkeys("\<C-x>\<C-o>", 'n')
866 endif
867endfunction
868
869"
870function s:handlerBase.onInsertLeave()
871 unlet s:runningHandler
872 let tempVars = l9#tempvariables#getList(s:TEMP_VARIABLES_GROUP)
873 call l9#tempvariables#end(s:TEMP_VARIABLES_GROUP)
874 call s:deactivateFufBuffer()
875 call fuf#saveDataFile(self.getModeName(), 'stats', self.stats)
876 execute self.windowRestoringCommand
877 let fOpen = exists('s:reservedCommand')
878 if fOpen
879 call self.onOpen(s:reservedCommand[0], s:reservedCommand[1])
880 unlet s:reservedCommand
881 endif
882 call self.onModeLeavePost(fOpen)
883 if exists('self.reservedMode')
884 call l9#tempvariables#setList(s:TEMP_VARIABLES_GROUP, tempVars)
885 call fuf#launch(self.reservedMode, self.lastPattern, self.partialMatching)
886 endif
887endfunction
888
889"
890function s:handlerBase.onCr(openType)
891 if pumvisible()
892 call feedkeys(printf("\<C-y>\<C-r>=fuf#getRunningHandler().onCr(%d) ? '' : ''\<CR>",
893 \ a:openType), 'n')
894 return
895 endif
896 if !empty(self.lastPattern)
897 call self.addStat(self.lastPattern, self.removePrompt(getline('.')))
898 endif
899 if !self.isOpenable(getline('.'))
900 " To clear i_<C-r> expression (fuf#getRunningHandler().onCr...)
901 echo ''
902 return
903 endif
904 let s:reservedCommand = [self.removePrompt(getline('.')), a:openType]
905 call feedkeys("\<Esc>", 'n') " stopinsert behavior is strange...
906endfunction
907
908"
909function s:handlerBase.onBs()
910 call feedkeys((pumvisible() ? "\<C-e>\<BS>" : "\<BS>"), 'n')
911endfunction
912
913"
914function s:getLastBlockLength(pattern, patternIsPath)
915 let separatorPos = strridx(a:pattern, g:fuf_patternSeparator)
916 if separatorPos >= 0
917 return len(a:pattern) - separatorPos
918 endif
919 if a:patternIsPath && a:pattern =~# '[/\\].'
920 return len(matchstr(a:pattern, '[^/\\]*.$'))
921 endif
922 return len(a:pattern)
923endfunction
924
925"
926function s:handlerBase.onDeleteWord()
927 let pattern = self.removePrompt(getline('.')[ : col('.') - 2])
928 let numBs = s:getLastBlockLength(pattern, 1)
929 call feedkeys((pumvisible() ? "\<C-e>" : "") . repeat("\<BS>", numBs), 'n')
930endfunction
931
932"
933function s:handlerBase.onPreviewBase(repeatable)
934 if self.getPreviewHeight() <= 0
935 return
936 elseif !pumvisible()
937 return
938 elseif !self.existsPrompt(getline('.'))
939 let word = self.removePrompt(getline('.'))
940 elseif !exists('self.lastFirstWord')
941 return
942 else
943 let word = self.lastFirstWord
944 endif
945 redraw
946 if a:repeatable && exists('self.lastPreviewInfo') && self.lastPreviewInfo.word ==# word
947 let self.lastPreviewInfo.count += 1
948 else
949 let self.lastPreviewInfo = {'word': word, 'count': 0}
950 endif
951 let lines = self.makePreviewLines(word, self.lastPreviewInfo.count)
952 let lines = lines[: self.getPreviewHeight() - 1]
953 call map(lines, 'substitute(v:val, "\t", repeat(" ", &tabstop), "g")')
954 call map(lines, 'strtrans(v:val)')
955 call map(lines, 'l9#snipTail(v:val, &columns - 1, s:ABBR_SNIP_MASK)')
956 echo join(lines, "\n")
957endfunction
958
959"
960function s:handlerBase.onSwitchMode(shift)
961 let modes = copy(fuf#getModeNames())
962 call map(modes, '{ "ranks": [ fuf#{v:val}#getSwitchOrder(), v:val ] }')
963 call filter(modes, 'v:val.ranks[0] >= 0')
964 call sort(modes, 'fuf#compareRanks')
965 let self.reservedMode = self.getModeName()
966 for i in range(len(modes))
967 if modes[i].ranks[1] ==# self.getModeName()
968 let self.reservedMode = modes[(i + a:shift) % len(modes)].ranks[1]
969 break
970 endif
971 endfor
972 call feedkeys("\<Esc>", 'n') " stopinsert doesn't work.
973endfunction
974
975"
976function s:handlerBase.onSwitchMatching()
977 let self.partialMatching = !self.partialMatching
978 let self.lastCol = -1
979 call setline('.', self.restorePrompt(self.lastPattern))
980 call feedkeys("\<End>", 'n')
981 "call self.onCursorMovedI()
982endfunction
983
984"
985function s:handlerBase.onRecallPattern(shift)
986 let patterns = map(copy(self.stats), 'v:val.pattern')
987 if !exists('self.indexRecall')
988 let self.indexRecall = -1
989 endif
990 let self.indexRecall += a:shift
991 if self.indexRecall < 0
992 let self.indexRecall = -1
993 elseif self.indexRecall >= len(patterns)
994 let self.indexRecall = len(patterns) - 1
995 else
996 call setline('.', self.getPrompt() . patterns[self.indexRecall])
997 call feedkeys("\<End>", 'n')
998 endif
999endfunction
1000
1001" }}}1
1002"=============================================================================
1003" INITIALIZATION {{{1
1004
1005augroup FufGlobal
1006 autocmd!
1007 autocmd BufLeave * let s:bufferCursorPosMap[bufnr('')] = getpos('.')
1008augroup END
1009
1010let s:bufferCursorPosMap = {}
1011
1012"
1013let s:DATA_FILE_VERSION = 400
1014
1015"
1016function s:checkDataFileCompatibility()
1017 if empty(g:fuf_dataDir)
1018 let s:dataFileAvailable = 0
1019 return
1020 endif
1021 let versionPath = l9#concatPaths([g:fuf_dataDir, 'VERSION'])
1022 let lines = l9#readFile(versionPath)
1023 if empty(lines)
1024 call l9#writeFile([s:DATA_FILE_VERSION], versionPath)
1025 let s:dataFileAvailable = 1
1026 elseif str2nr(lines[0]) == s:DATA_FILE_VERSION
1027 let s:dataFileAvailable = 1
1028 else
1029 call fuf#echoWarning(printf(
1030 \ "=======================================================\n" .
1031 \ " Existing data files for FuzzyFinder is no longer \n" .
1032 \ " compatible with this version of FuzzyFinder. Remove \n" .
1033 \ " %-53s\n" .
1034 \ "=======================================================\n" ,
1035 \ string(g:fuf_dataDir)))
1036 call l9#inputHl('Question', 'Press Enter')
1037 let s:dataFileAvailable = 0
1038 endif
1039endfunction
1040
1041call s:checkDataFileCompatibility()
1042
1043" }}}1
1044"=============================================================================
1045" vim: set fdm=marker:
1046