diff --git a/autoload/ctrlp.vim b/autoload/ctrlp.vim index 19ac1463..959df8cf 100644 --- a/autoload/ctrlp.vim +++ b/autoload/ctrlp.vim @@ -142,8 +142,19 @@ let [s:lcmap, s:prtmaps] = ['nn ', { \ 'MarkToOpen()': [''], \ 'OpenMulti()': [''], \ 'PrtExit()': ['', '', ''], + \ 'PrtNoop()': [''], \ }] +let s:scriptpath = expand(':p:h') +let s:pymatcher = 0 +if has('autocmd') && has('python') + py import sys + exe 'python sys.path.insert( 0, "' . escape(s:scriptpath, '\') . '/../python" )' + py from ctrlp.matcher import CtrlPMatcher + py ctrlp = CtrlPMatcher() + let s:pymatcher = 1 +en + if !has('gui_running') cal add(s:prtmaps['PrtBS()'], remove(s:prtmaps['PrtCurLeft()'], 0)) en @@ -468,8 +479,8 @@ fu! s:MatchIt(items, pat, limit, exc) en | cat | brea | endt if a:limit > 0 && len(lines) >= a:limit | brea | en endfo - let s:mdata = [s:dyncwd, s:itemtype, s:regexp, s:sublist(a:items, id, -1)] - retu lines + + cal ctrlp#process(lines, a:pat, 0, s:sublist(a:items, id, -1)) endf fu! s:MatchedItems(items, pat, limit) @@ -486,13 +497,17 @@ fu! s:MatchedItems(items, pat, limit) \ 'crfile': exc, \ 'regex': s:regexp, \ }] : [items, a:pat, a:limit, s:mmode(), s:ispath, exc, s:regexp] - let lines = call(s:matcher['match'], argms, s:matcher) + call(s:matcher['match'], argms, s:matcher) + elsei s:pymatcher && s:lazy > 1 + py < 1) + \ || s:nolim == 1 || ( s:itemtype == 2 && s:mrudef ) \ || ( s:itemtype =~ '\v^(1|2)$' && s:prompt == ['', '', ''] ) || !s:dosort endf @@ -2262,6 +2290,26 @@ fu! ctrlp#init(type, ...) cal s:BuildPrompt(1) if s:keyloop | cal s:KeyLoop() | en endf + +fu! ctrlp#process(lines, pat, split, subitems) + if !exists('s:init') | retu | en + + let s:matches = len(a:lines) + unl! s:did_exp + + let pat = a:pat + + if a:split | let pat = s:SplitPattern(pat) | en + + let s:mdata = [s:dyncwd, s:itemtype, s:regexp, a:subitems] + + cal s:Render(a:lines, pat) +endf + +fu! ctrlp#forcecursorhold() + cal feedkeys("\") +endf + " - Autocmds {{{1 if has('autocmd') aug CtrlPAug diff --git a/python/ctrlp/__init__.py b/python/ctrlp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ctrlp/matcher.py b/python/ctrlp/matcher.py new file mode 100644 index 00000000..20a1b901 --- /dev/null +++ b/python/ctrlp/matcher.py @@ -0,0 +1,390 @@ +from Queue import Empty, Queue +from threading import Thread +from ctrlp.regex import from_vim, is_escaped + +import logging, re, os, tempfile + +novim = False +try: + import vim +except ImportError: + novim = True + +class CtrlPMatcher: + def __init__(self, debug=False): + if novim: + raise ImportError("No module named vim") + + self.queue = Queue() + self.patterns = [] + self.thread = None + + self.lastpat = None + self.lastmmode = None + self.lastispath = None + self.lastregexp = None + + self.logger = logging.getLogger('ctrlp') + hdlr = logging.FileHandler(os.path.join(tempfile.gettempdir(), 'ctrlp-py.log')) + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + hdlr.setFormatter(formatter) + self.logger.addHandler(hdlr) + + if debug: + self.logger.setLevel(logging.DEBUG) + + def filter(self, items, pat, limit, mmode, ispath, crfile, regexp): + if not pat: + self.logger.debug("No pattern, returning original items") + self.queue.put(self.initialList(items, limit, ispath, crfile), timeout=1) + + self.process(pat) + + return + + self.logger.debug("Filtering {number} items using {pat}".format(number = len(items), pat=pat)) + + self.process(pat) + + if self.lastpat == pat and self.lastmmode == mmode \ + and self.lastispath == ispath and self.lastregexp == regexp: + if self.process(pat) and self.queue.qsize() == 0 and not self.thread.isAlive(): + self.logger.debug("Thread job is processed for {pat}".format(pat=pat)) + self.lastpat = None + elif self.thread.isAlive() or self.queue.qsize() > 0: + self.logger.debug("Waiting for thread job for {pat}".format(pat=pat)) + self.forceCursorHold() + else: + self.logger.debug("The same pattern '{pat}'".format(pat=pat)) + elif pat: + self.logger.debug("Starting thread for {pat}".format(pat=pat)) + self.patterns.append(pat) + + mru = vim.bindeval('ctrlp#mrufiles#list()') + mru = list(mru)[:50] if isinstance(mru, vim.List) else [] + + self.thread = Thread(target=thread_worker, args=( + self.queue, items, pat, limit, + mmode, ispath, crfile, regexp, mru, + vim.bindeval('&ic'), vim.bindeval('&scs'), self.logger + )) + self.thread.daemon = True + self.thread.start() + + self.lastpat = pat + self.lastmmode = mmode + self.lastispath = ispath + self.lastregexp = regexp + + self.forceCursorHold() + + def process(self, pat): + queue = [] + while True: + try: + queue.append(self.queue.get(False)) + self.queue.task_done() + except Empty: + break + + if not queue: + self.logger.debug("Empty queue") + return False + + data = None + + for d in queue: + if d["pat"]: + try: + index = self.patterns.index(d["pat"]) + self.patterns = self.patterns[index+1:] + data = d + except ValueError: + continue + else: + data = d + self.lastpat = None + self.patterns = [] + break + + if not data: + self.logger.debug("No valid data entry") + return False + + callback = vim.bindeval('function("ctrlp#process")') + lines = vim.List(data["items"]) + subitems = vim.List(data["subitems"]) + + callback(lines, pat, 1, subitems) + + if data["pat"] == pat: + self.queue = Queue() + + return True + + def forceCursorHold(self): + vim.bindeval('function("ctrlp#forcecursorhold")')() + + def initialList(self, items, limit, ispath, crfile): + mru = None + if ispath: + mru = vim.bindeval('ctrlp#mrufiles#list()') + + if isinstance(mru, vim.List) and mru: + mrudict = {} + mrucount = len(mru) + index = 0 + for f in mru: + mrudict[f] = index + index += 1 + + recentCount = 0 + restCount = 0 + + recent = [] + rest = [] + for item in items: + if item == crfile: + continue + + if mrudict.get(item, -1) != -1: + recent.append(item) + recentCount += 1 + mrucount -= 1 + + elif restCount + recentCount < limit: + rest.append(item) + restCount += 1 + + if recentCount == limit or not mrucount and restCount + recentCount >= limit: + break + + recent = sorted(recent, cmp=lambda a, b: mrudict.get(a, -1) - mrudict.get(b, -1)) + return {"items": recent + rest[:limit - restCount], "subitems": items[recentCount-1:], "pat": ""} + + else: + return {"items": items[:limit], "subitems": items[limit-1:], "pat": ""} + + +def thread_worker(queue, items, pat, limit, mmode, ispath, crfile, regexp, mru, ic, scs, logger): + if ispath and mmode == 'filename-only': + semi = 0 + while semi != -1: + semi = pat.find(';', semi) + if semi != -1 and is_escaped(pat, semi): + semi += 1 + else: + break + else: + semi = -1 + + if semi != -1: + pats = [pat[:semi], pat[semi+1:]] if pat[semi+1:] else [pat[:semi]] + else: + pats = [pat] + + patterns = [] + if regexp: + logger.debug("Regex matching") + patterns = [from_vim(p, ignorecase=ic, smartcase=scs) for p in pats] + else: + logger.debug("Fuzzy matching") + flags = 0 + if ic: + if scs: + upper = any(c.isupper() for c in pat) + if not upper: + flags = re.I + else: + flags = re.I + + for p in pats: + chars = [re.escape(c) for c in p] + builder = lambda c: c + '[^' + c + ']*?' + + patterns.append(re.compile(''.join(map(builder, chars)), flags)) + + index = 0 + mrudict = {} + for f in mru: + mrudict[f] = index + index += 1 + + recent = [] + if ispath and mrudict: + for item in items: + recent.append(item if mrudict.has_key(item) else None) + + count = 0 + matchedItems = [] + skip = {} + itemIds = [0, 0] + + logger.debug("Matching against {number} items using {pat}".format(number=len(items), pat=pat)) + + if recent: + count = reducer(recent, patterns, limit, mmode, ispath, crfile, matchedItems, count, skip, itemIds) + + if count < limit - 1: + count = reducer(items, patterns, limit, mmode, ispath, crfile, matchedItems, count, skip, itemIds) + + matchedItems = matchedItems[:limit] + matchedItems = sorted(matchedItems, cmp=sort_items(crfile, mmode, ispath, + mrudict, len(matchedItems))) + + if limit: + matchedItems = matchedItems[:limit] + + queue.put({ + "items": [i["line"] for i in matchedItems], + "subitems": items[max(itemIds)], + "pat": pat + }, timeout=1) + logger.debug("Got {number} matched items using {pat}".format(number=len(matchedItems), pat=pat)) + + +def reducer(items, patterns, limit, mmode, ispath, crfile, matchedItems, count, skip, itemIds): + index = -1 + if ispath and (mmode == 'filename-only' or mmode == 'full-line'): + for item in items: + index += 1 + + if item is None: + continue + + if itemIds[0] < index: + itemIds[0] = index + + if skip.get(index, False) or ispath and item == crfile: + continue + + basename = os.path.basename(item) + span = () + match = None + + if mmode == 'filename-only': + match = patterns[0].search(basename) + if match: + span = match.span() + + if len(patterns) == 2: + dirname = os.path.dirname(item) + match = patterns[1].search(dirname) + + elif mmode == 'full-line': + match = patterns[0].search(basename) + + if not match: + continue + + if not span: + span = match.span() + + matchedItems.append({"line": item, "matlen": span[1] - span[0]}) + skip[index] = True + + if limit and count >= limit: + break + + count += 1 + + index = -1 + if count < limit - 1 and (not ispath or mmode != 'filename-only'): + for item in items: + index += 1 + + if item is None: + continue + + if itemIds[1] < index: + itemIds[1] = index + + if skip.get(index, False) or ispath and item == crfile: + continue + + if mmode == 'first-non-tab': + match = patterns[0].search(re.split('\t+', item)[0]) + elif mmode == 'until-last-tab': + match = patterns[0].search(re.split('\t+[^\t]+$', item)[0]) + else: + match = patterns[0].search(item) + + if match is None: + continue + + span = match.span() + matchedItems.append({"line": item, "matlen": span[1] - span[0]}) + + if limit and count >= limit: + break + + count += 1 + + return count + + +def sort_items(crfile, mmode, ispath, mrudict, total): + crdir = os.path.dirname(crfile) + + def cmp_func(a, b): + line1 = a["line"] + line2 = b["line"] + len1 = len(line1) + len2 = len(line2) + + lanesort = 0 if len1 == len2 else 1 if len1 > len2 else -1 + + len1 = a["matlen"] + len2 = b["matlen"] + + patsort = 0 if len1 == len2 else 1 if len1 > len2 else -1 + + if ispath: + ms = [] + + fnlen = 0 + mtime = 0 + pcomp = 0 + + if total < 21: + len1 = len(os.path.basename(line1)) + len2 = len(os.path.basename(line2)) + fnlen = 0 if len1 == len2 else 1 if len1 > len2 else -1 + + if mmode == 'full-line': + try: + len1 = os.path.getmtime(line1) + len2 = os.path.getmtime(line2) + mtime = 0 if len1 == len2 else 1 if len1 > len2 else -1 + + dir1 = os.path.dirname(line1) + dir2 = os.path.dirname(line2) + + if dir1.endswith(crdir) and not dir2.endswith(crdir): + pcomp = -1 + elif dir2.endswith(crdir) and not dir1.endswith(crdir): + pcomp = 1 + + except OSError: + pass + + mrucomp = 0 + if mrudict: + len1 = mrudict.get(line1, -1) + len2 = mrudict.get(line2, -1) + + mrucomp = 0 if len1 == len2 else 1 if len1 == -1 else -1 if len2 == -1 \ + else 1 if len1 > len2 else -1 + + ms.extend([fnlen, mtime, pcomp, patsort, mrucomp]) + mp = [2 if ms[0] else 0] + mp.append(1 + (mp[0] if mp[0] else 1) if ms[1] else 0) + mp.append(1 + (mp[0] + mp[1] if mp[0] + mp[1] else 1) if ms[2] else 0) + mp.append(1 + (mp[0] + mp[1] + mp[2] if mp[0] + mp[1] + mp[2] else 1) if ms[3] else 0) + mp.append(1 + (mp[0] + mp[1] + mp[2] + mp[3] if mp[0] + mp[1] + mp[2] + mp[3] else 1) if ms[4] else 0) + + return lanesort + reduce(lambda x, y: x + y[0]*y[1], zip(ms, mp), 0) + else: + return lanesort + patsort * 2 + + return cmp_func diff --git a/python/ctrlp/regex.py b/python/ctrlp/regex.py new file mode 100644 index 00000000..452ae813 --- /dev/null +++ b/python/ctrlp/regex.py @@ -0,0 +1,389 @@ +import re + +def from_vim(pat, ignorecase=False, smartcase=False): + r""" + Returns a pattern object based on the vim-style regular expression pattern string + + >>> from_vim('\w\+').pattern == '\w+' + True + >>> from_vim('foo\\').pattern == r'foo\\' + True + >>> from_vim('foo\(').pattern == r'foo\(' + True + >>> from_vim('foo(').pattern == r'foo\(' + True + >>> from_vim('foo\[').pattern == r'foo\[' + True + >>> from_vim('foo[').pattern == r'foo\[' + True + >>> from_vim('foo\(a').pattern == r'foo\(a' + True + >>> from_vim('foo(a').pattern == r'foo\(a' + True + >>> from_vim('foo\[a').pattern == r'foo\[a' + True + >>> from_vim('foo[a').pattern == r'foo\[a' + True + >>> from_vim('\zsfoo\ze').pattern == r'foo' + True + >>> from_vim('\w\+').flags + 0 + >>> from_vim('\c\w\+').flags + 2 + >>> from_vim('\C\w\+').flags + 0 + >>> from_vim('\C\w\+', ignorecase=True).flags + 0 + >>> from_vim('\w\+', ignorecase=True).flags + 2 + >>> from_vim('a', ignorecase=True, smartcase=True).flags + 2 + >>> from_vim('A', ignorecase=True, smartcase=True).flags + 0 + >>> from_vim('foo\=').pattern == 'foo?' + True + >>> from_vim('foo\?').pattern == 'foo?' + True + >>> from_vim('foo\{1,5}b').pattern == 'foo{1,5}b' + True + >>> from_vim('foo\{1,\}b').pattern == 'foo{1,}b' + True + >>> from_vim('foo\{-\}b').pattern == 'foo*?b' + True + >>> from_vim('foo\{-,1\}b').pattern == 'foo??b' + True + >>> from_vim('foo\{-1,\}b').pattern == 'foo+?b' + True + >>> from_vim('foo{1,}b').pattern == 'foo\{1,}b' + True + >>> from_vim('foo\>').pattern == r'foo\b' + True + >>> from_vim('\>> from_vim('\').pattern == r'\bfoo\b' + True + >>> from_vim('foo\|bar').pattern == r'foo|bar' + True + >>> from_vim('\(foo\)').pattern == r'(foo)' + True + >>> from_vim('\(f(o)o\)').pattern == r'(f\(o\)o)' + True + >>> from_vim(r'\%(foo\)').pattern == r'(?:foo)' + True + >>> from_vim(r'\%(fo\(oba\)r\)').pattern == r'(?:fo(oba)r)' + True + >>> from_vim(r'foo\@=').pattern == r'fo(?=o)' + True + >>> from_vim('\(foo\)\@=').pattern == r'(?=foo)' + True + >>> from_vim(r'\%(foo\)\@=').pattern == r'(?=foo)' + True + >>> from_vim('foo\@!').pattern == r'fo(?!o)' + True + >>> from_vim('\(foo\)\@<=').pattern == r'(?<=foo)' + True + >>> from_vim(r'\%(foo\)\@>> from_vim(r'[a-z]').pattern == r'[a-z]' + True + >>> from_vim(r'\x').pattern == r'[0-9A-Fa-f]' + True + >>> from_vim(r'\X').pattern == r'[^0-9A-Fa-f]' + True + >>> from_vim(r'\o').pattern == r'[0-7]' + True + >>> from_vim(r'\O').pattern == r'[^0-7]' + True + >>> from_vim(r'\h').pattern == r'[A-Za-z_]' + True + >>> from_vim(r'\H').pattern == r'[^A-Za-z_]' + True + >>> from_vim(r'\a').pattern == r'[A-Za-z]' + True + >>> from_vim(r'\A').pattern == r'[^A-Za-z]' + True + >>> from_vim(r'\l').pattern == r'[a-z]' + True + >>> from_vim(r'\L').pattern == r'[^a-z]' + True + >>> from_vim(r'\u').pattern == r'[A-Z]' + True + >>> from_vim(r'\U').pattern == r'[^A-Z]' + True + """ + + flags = 0 + + if pat.find("\c") != -1: + pat = pat.replace("\c", "") + flags |= re.IGNORECASE + elif pat.find("\C") != -1: + pat = pat.replace("\C", "") + elif ignorecase: + if smartcase: + if not any(c.isupper() for c in pat): + flags |= re.IGNORECASE + else: + flags |= re.IGNORECASE + + regex = process_group(pat) + + return re.compile(regex, flags) + +def process_group(pat): + special = False + index = 0 + + regex = r"" + incurly = False + nongreedy = False + nomemory = False + + skip = {} + + for char in pat: + if skip.get(index, False): + index += 1 + continue + + if special: + special = False + + if char == r'+': + regex += char + elif char == r'=' or char == r'?': + regex += r'?' + elif char == r'<' or char == r'>': + regex += r'\b' + elif char == r'|': + regex += char + elif char == r'{': + if pat[index+1] == '-': + skip[index+1] = True + + if non_greedy_skip(pat, index, skip): + regex += r'*?' + elif pat[index+2] == '1' and pat[index+3] == ',' and non_greedy_skip(pat, index + 2, skip): + skip.update(skip.fromkeys([index+2, index+3], True)) + regex += r'+?' + elif pat[index+2] == ',' and pat[index+3] == '1' and non_greedy_skip(pat, index + 2, skip): + skip.update(skip.fromkeys([index+2, index+3], True)) + regex += r'??' + else: + nongreedy = True + else: + incurly = True + regex += r'{' + + elif char == r'%': + if pat[index+1] == '(': + special = True + nomemory = True + elif char == r'(': + closing = find_matching(pat, index, r'\(', r'\)') + + if closing == -1: + regex += r'\(' + else: + regex += r'(' + if pat[closing+2:closing+5] == r'\@=': + regex += r'?=' + skip.update(skip.fromkeys([closing+2, closing+3, closing+4], True)) + elif pat[closing+2:closing+5] == r'\@!': + regex += r'?!' + skip.update(skip.fromkeys([closing+2, closing+3, closing+4], True)) + elif pat[closing+2:closing+6] == r'\@<=': + regex += r'?<=' + skip.update(skip.fromkeys([closing+2, closing+3, closing+4, closing+5], True)) + elif pat[closing+2:closing+6] == r'\@>> find_matching("foo \( bar \) alpha", 5, "\(", "\)") + 11 + >>> find_matching("foo \( bar alpha", 5, "\(", "\)") + -1 + >>> find_matching("foo \( bar \( inner 1 \) after \) alpha", 5, "\(", "\)") + 31 + >>> find_matching("foo \( bar \( in \( n \) er 1 \) after \) alpha", 5, "\(", "\)") + 39 + >>> find_matching("foo \( bar \( inner 1 \) after \) alpha \( another \( one \) no \) yes ", 5, "\(", "\)") + 31 + >>> find_matching("foo \( bar \( inner 1 after \) alpha", 5, "\(", "\)") + 28 + >>> find_matching(r"foo \( bar \\) inner 1 after \) alpha", 5, "\(", "\)") + 29 + >>> find_matching(r"foo \( bar ", 5, "\(", "\)") + -1 + """ + + cursor = end = start - len(opening) + + while True: + while end != -1: + end = string.find(closing, end) + if end != -1 and is_escaped(string, end): + end += 1 + else: + break + + while cursor != -1: + cursor = string.find(opening, cursor) + if cursor != -1 and is_escaped(string, cursor): + cursor += 1 + else: + break + + if not start: + start = cursor + + if end < 0: + end = string.rfind(closing) + break + + next_cursor = cursor + 1 + while next_cursor != -1: + next_cursor = string.find(opening, next_cursor) + if next_cursor != -1 and is_escaped(string, next_cursor): + next_cursor = next_cursor + 1 + else: + break + + if cursor > -1 and next_cursor > -1 and next_cursor < end and cursor < end: + cursor += 1 + end += 1 + else: + break + + return end + +def is_escaped(string, position): + r""" + Checks whether the entity at the given position is escaped + + >>> is_escaped(r"foo bar", 4) + False + >>> is_escaped(r"foo \\bar", 5) + True + >>> is_escaped(r"foo \\\\bar", 6) + False + >>> is_escaped(r"foo \\\\\\bar", 7) + True + """ + + chars = list(string[:position]) + chars.reverse() + + counter = 0 + for c in chars: + if c == '\\': + counter += 1 + else: + break + + return counter % 2 != 0 + +def non_greedy_skip(chars, index, skip): + if chars[index+2] == '}': + skip[index+2] = True + return True + elif chars[index+2] == '\\' and chars[index+3] == '}': + skip[index+2] = True + skip[index+3] = True + return True + skip[index+2] = True + + return False + +if __name__ == "__main__": + import doctest + doctest.testmod()