├── .gitignore ├── LICENSE ├── README.md ├── autoload ├── fuzzycomt.c ├── fuzzycomt.h ├── matcher.vim └── setup.py ├── install.sh └── install_windows.bat /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | autoload/build 3 | autoload/fuzzycomt.so 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010-2012 Wincent Colaiuta. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 16 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 19 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 20 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 22 | POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CtrlP C matching extension 2 | 3 | This is a [ctrlp.vim][ctrlp] extension which can be used to get different matching algorithm, written in C language with a small portion of Python (only to access C module). 4 | 5 | This extension uses an adapted version of [CommandT][commandt] matching, big thanks to @wincent! 6 | 7 | ## Advantages 8 | - Matcher, written in C, can provide significant speed improvement when working on large projects with 10000+ files, e.g [Metasploit][metasploit]. Dont forget to set ``g:ctrlp_max_files`` option to 0 or 10000+ if you're working on such projects. 9 | - In some cases you can get more precise matching results ( e.g. when trying to match exact file name like ``exe.rb``) 10 | 11 | ## Drawbacks 12 | 13 | There are no real drawbacks, but I need to point out some things that may not work as you expected: 14 | 15 | - ``Regex`` mode doesn't use C matching and probably will never use it. If you will use it with this extension it will fall back to ``ctrlp.vim`` matching and may be slow on large projects. 16 | 17 | ## Installation 18 | 19 | 1. Get extension files with your favorite method. Example for Vundle: 20 | 21 | ```vim 22 | Plugin 'JazzCore/ctrlp-cmatcher' 23 | ``` 24 | 2. Compile C extension. 25 | If you are getting any errors on this stage you can try the manual installation guide located [here][manual]. 26 | 27 | * On Linux/Unix systems: 28 | 29 | First, get Python header files. Example for Debian/Ubuntu: 30 | 31 | ```sh 32 | [sudo] apt-get install python-dev 33 | ``` 34 | 35 | Then run the installation script: 36 | 37 | ```sh 38 | cd ~/.vim/bundle/ctrlp-cmatcher 39 | ./install.sh 40 | ``` 41 | 42 | * On OS X (tested with 10.9.2 Mavericks): 43 | 44 | First [fix the compiler](http://stackoverflow.com/a/22322645/6962): 45 | 46 | ```sh 47 | export CFLAGS=-Qunused-arguments 48 | export CPPFLAGS=-Qunused-arguments 49 | ``` 50 | 51 | Then run the installation script: 52 | 53 | ```sh 54 | cd ~/.vim/bundle/ctrlp-cmatcher 55 | ./install.sh 56 | ``` 57 | 58 | * On Windows: 59 | 60 | Installation is similar to Linux version, but it can be more complicated because of weird errors during compilation. 61 | 62 | First of all, make sure that you have ``python`` in your ``%PATH%``. 63 | 64 | Also you will need MinGW compiler suite installed. Dont forget to add ``C:\MinGW\bin`` to your ``%PATH%``. 65 | 66 | Then go to ``ctrlp-cmatcher`` dir and run the installation script: 67 | 68 | ``` 69 | install_windows.bat 70 | ``` 71 | 72 | If you are getting __gcc: error: unrecognized command line option '-mno-cygwin'__ error, follow [this fix](http://stackoverflow.com/questions/6034390/compiling-with-cython-and-mingw-produces-gcc-error-unrecognized-command-line-o). 73 | 74 | 3. Edit your ``.vimrc``: 75 | 76 | Add following line: 77 | 78 | ```vim 79 | let g:ctrlp_match_func = {'match' : 'matcher#cmatch' } 80 | ``` 81 | 82 | 4. All done! 83 | 84 | [ctrlp]: https://github.com/kien/ctrlp.vim 85 | [commandt]: https://github.com/wincent/Command-T 86 | [metasploit]: https://github.com/rapid7/metasploit-framework 87 | [manual]: https://github.com/JazzCore/ctrlp-cmatcher/wiki/Manual-installation 88 | -------------------------------------------------------------------------------- /autoload/fuzzycomt.c: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Stanislav Golovanov 2 | // Matching algorithm by Wincent Colaiuta, 2010. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 17 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | // POSSIBILITY OF SUCH DAMAGE. 24 | 25 | #include "float.h" 26 | #include "fuzzycomt.h" 27 | 28 | // Forward declaration for ctrlp_get_line_matches 29 | matchobj_t ctrlp_find_match(PyObject* str, PyObject* abbrev, char *mmode); 30 | 31 | void ctrlp_get_line_matches(PyObject* paths, 32 | PyObject* abbrev, 33 | matchobj_t matches[], 34 | char *mmode) 35 | { 36 | int i; 37 | int max; 38 | // iterate over lines and get match score for every line 39 | for (i = 0, max = PyList_Size(paths); i < max; i++) { 40 | PyObject* path = PyList_GetItem(paths, i); 41 | matchobj_t match; 42 | match = ctrlp_find_match(path, abbrev, mmode); 43 | matches[i] = match; 44 | } 45 | } 46 | 47 | char *strduplicate(const char *s) { 48 | char *d = malloc (strlen (s) + 1); 49 | if (d == NULL) 50 | return NULL; 51 | strcpy (d,s); 52 | return d; 53 | } 54 | 55 | char *slashsplit(char *line) { 56 | char *pch, *linedup; 57 | char *fname = ""; 58 | 59 | // we need to create a copy of input string because strtok() changes string 60 | // while splitting. Need to call free() when linedup is not needed. 61 | linedup = strduplicate(line); 62 | 63 | pch = strtok(linedup, "/"); 64 | 65 | while (pch != NULL) 66 | { 67 | fname = pch; 68 | pch = strtok(NULL, "/"); 69 | } 70 | 71 | // We need to get a copy of a filename because fname is a pointer to the 72 | // start of filename in linedup string which will be free'd. We need to 73 | // call free() when return value of func will not be needed. 74 | char *retval = strduplicate(fname); 75 | 76 | free(linedup); 77 | 78 | return retval; 79 | } 80 | 81 | // comparison function for use with qsort 82 | int ctrlp_comp_alpha(const void *a, const void *b) { 83 | matchobj_t a_val = *(matchobj_t *)a; 84 | matchobj_t b_val = *(matchobj_t *)b; 85 | 86 | char *a_p = PyString_AsString(a_val.str); 87 | long a_len = PyString_Size(a_val.str); 88 | char *b_p = PyString_AsString(b_val.str); 89 | long b_len = PyString_Size(b_val.str); 90 | 91 | int order = 0; 92 | if (a_len > b_len) { 93 | order = strncmp(a_p, b_p, b_len); 94 | if (order == 0) 95 | order = 1; // shorter string (b) wins 96 | } 97 | else if (a_len < b_len) { 98 | order = strncmp(a_p, b_p, a_len); 99 | if (order == 0) 100 | order = -1; // shorter string (a) wins 101 | } 102 | else { 103 | order = strncmp(a_p, b_p, a_len); 104 | } 105 | 106 | return order; 107 | } 108 | 109 | int ctrlp_comp_score_alpha(const void *a, const void *b) { 110 | matchobj_t a_val = *(matchobj_t *)a; 111 | matchobj_t b_val = *(matchobj_t *)b; 112 | double a_score = a_val.score; 113 | double b_score = b_val.score; 114 | if (a_score > b_score) 115 | return -1; // a scores higher, a should appear sooner 116 | else if (a_score < b_score) 117 | return 1; // b scores higher, a should appear later 118 | else 119 | return ctrlp_comp_alpha(a, b); 120 | } 121 | 122 | double ctrlp_recursive_match(matchinfo_t *m, // sharable meta-data 123 | long haystack_idx, // where in the path string to start 124 | long needle_idx, // where in the needle string to start 125 | long last_idx, // location of last matched character 126 | double score) // cumulative score so far 127 | { 128 | double seen_score = 0; // remember best score seen via recursion 129 | long i, j, distance; 130 | int found; 131 | double score_for_char; 132 | long memo_idx = haystack_idx; 133 | 134 | // do we have a memoized result we can return? 135 | double memoized = m->memo[needle_idx * m->needle_len + memo_idx]; 136 | if (memoized != DBL_MAX) 137 | return memoized; 138 | 139 | // bail early if not enough room (left) in haystack for (rest of) needle 140 | if (m->haystack_len - haystack_idx < m->needle_len - needle_idx) { 141 | score = 0.0; 142 | goto memoize; 143 | } 144 | 145 | for (i = needle_idx; i < m->needle_len; i++) { 146 | char c = m->needle_p[i]; 147 | found = 0; 148 | 149 | // similar to above, we'll stop iterating when we know we're too close 150 | // to the end of the string to possibly match 151 | for (j = haystack_idx; 152 | j <= m->haystack_len - (m->needle_len - i); 153 | j++, haystack_idx++) { 154 | 155 | char d = m->haystack_p[j]; 156 | if (d == '.') { 157 | if (j == 0 || m->haystack_p[j - 1] == '/') { 158 | m->dot_file = 1; // this is a dot-file 159 | } 160 | } else if (d >= 'A' && d <= 'Z') { 161 | d += 'a' - 'A'; // add 32 to downcase 162 | } 163 | 164 | if (c == d) { 165 | found = 1; 166 | 167 | // calculate score 168 | score_for_char = m->max_score_per_char; 169 | distance = j - last_idx; 170 | 171 | if (distance > 1) { 172 | double factor = 1.0; 173 | char last = m->haystack_p[j - 1]; 174 | char curr = m->haystack_p[j]; // case matters, so get again 175 | if (last == '/') 176 | factor = 0.9; 177 | else if (last == '-' || 178 | last == '_' || 179 | last == ' ' || 180 | (last >= '0' && last <= '9')) 181 | factor = 0.8; 182 | else if (last >= 'a' && last <= 'z' && 183 | curr >= 'A' && curr <= 'Z') 184 | factor = 0.8; 185 | else if (last == '.') 186 | factor = 0.7; 187 | else 188 | // if no "special" chars behind char, factor diminishes 189 | // as distance from last matched char increases 190 | factor = (1.0 / distance) * 0.75; 191 | score_for_char *= factor; 192 | } 193 | 194 | if (++j < m->haystack_len) { 195 | // bump cursor one char to the right and 196 | // use recursion to try and find a better match 197 | double sub_score = ctrlp_recursive_match(m, j, i, last_idx, score); 198 | if (sub_score > seen_score) 199 | seen_score = sub_score; 200 | } 201 | 202 | score += score_for_char; 203 | last_idx = ++haystack_idx; 204 | break; 205 | } 206 | } 207 | 208 | if (!found) { 209 | score = 0.0; 210 | goto memoize; 211 | } 212 | } 213 | 214 | score = score > seen_score ? score : seen_score; 215 | 216 | memoize: 217 | m->memo[needle_idx * m->needle_len + memo_idx] = score; 218 | return score; 219 | } 220 | 221 | PyObject* ctrlp_fuzzycomt_match(PyObject* self, PyObject* args) { 222 | PyObject *paths, *abbrev, *returnlist; 223 | Py_ssize_t limit; 224 | char *mmode; 225 | int i; 226 | int max; 227 | 228 | if (!PyArg_ParseTuple(args, "OOns", &paths, &abbrev, &limit, &mmode)) { 229 | return NULL; 230 | } 231 | returnlist = PyList_New(0); 232 | 233 | // Type checking 234 | if (PyList_Check(paths) != 1) { 235 | PyErr_SetString(PyExc_TypeError,"expected a list"); 236 | return 0; 237 | } 238 | 239 | if (PyString_Check(abbrev) != 1) { 240 | PyErr_SetString(PyExc_TypeError,"expected a string"); 241 | return 0; 242 | } 243 | 244 | matchobj_t matches[PyList_Size(paths)]; 245 | 246 | if ( (limit > PyList_Size(paths)) || (limit == 0) ) { 247 | limit = PyList_Size(paths); 248 | } 249 | 250 | if ( PyString_Size(abbrev) == 0) { 251 | // if string is empty - just return first (:param limit) lines 252 | PyObject *initlist; 253 | 254 | initlist = PyList_GetSlice(paths,0,limit); 255 | return initlist; 256 | } 257 | else { 258 | // find matches and place them into matches array. 259 | ctrlp_get_line_matches(paths,abbrev, matches, mmode); 260 | 261 | // sort array of struct by struct.score key 262 | qsort(matches, PyList_Size(paths), 263 | sizeof(matchobj_t), 264 | ctrlp_comp_score_alpha); 265 | } 266 | 267 | for (i = 0, max = PyList_Size(paths); i < max; i++) { 268 | if (i == limit) 269 | break; 270 | // generate python dicts { 'line' : line, 'value' : value } 271 | // and place dicts to list 272 | PyObject *container; 273 | container = PyDict_New(); 274 | // TODO it retuns non-encoded string. So cyrillic literals 275 | // arent properly showed. 276 | // There are PyString_AsDecodedObject, it works in interactive 277 | // session but it fails in Vim for some reason 278 | // (probably because we dont know what encoding vim returns) 279 | PyDict_SetItemString(container, "line", matches[i].str); 280 | PyDict_SetItemString(container, 281 | "value", 282 | PyFloat_FromDouble(matches[i].score)); 283 | PyList_Append(returnlist,container); 284 | } 285 | 286 | return returnlist; 287 | } 288 | 289 | PyObject* ctrlp_fuzzycomt_sorted_match_list(PyObject* self, PyObject* args) { 290 | PyObject *paths, *abbrev, *returnlist; 291 | Py_ssize_t limit; 292 | char *mmode; 293 | int i; 294 | int max; 295 | 296 | if (!PyArg_ParseTuple(args, "OOns", &paths, &abbrev, &limit, &mmode)) { 297 | return NULL; 298 | } 299 | returnlist = PyList_New(0); 300 | 301 | // Type checking 302 | if (PyList_Check(paths) != 1) { 303 | PyErr_SetString(PyExc_TypeError,"expected a list"); 304 | return 0; 305 | } 306 | 307 | if (PyString_Check(abbrev) != 1) { 308 | PyErr_SetString(PyExc_TypeError,"expected a string"); 309 | return 0; 310 | } 311 | 312 | matchobj_t matches[PyList_Size(paths)]; 313 | 314 | if ( (limit > PyList_Size(paths)) || (limit == 0) ) { 315 | limit = PyList_Size(paths); 316 | } 317 | 318 | if ( PyString_Size(abbrev) == 0) { 319 | // if string is empty - just return first (:param limit) lines 320 | PyObject *initlist; 321 | 322 | initlist = PyList_GetSlice(paths,0,limit); 323 | return initlist; 324 | } 325 | else { 326 | // find matches and place them into matches array. 327 | ctrlp_get_line_matches(paths,abbrev, matches, mmode); 328 | 329 | // sort array of struct by struct.score key 330 | qsort(matches, 331 | PyList_Size(paths), 332 | sizeof(matchobj_t), 333 | ctrlp_comp_score_alpha); 334 | } 335 | 336 | for (i = 0, max = PyList_Size(paths); i < max; i++) { 337 | if (i == limit) 338 | break; 339 | if ( matches[i].score> 0 ) { 340 | // TODO it retuns non-encoded string. So cyrillic literals 341 | // arent properly showed. 342 | // There are PyString_AsDecodedObject, it works in interactive 343 | // session but it fails in Vim for some reason 344 | // (probably because we dont know what encoding vim returns) 345 | PyList_Append(returnlist,matches[i].str); 346 | } 347 | } 348 | 349 | return returnlist; 350 | } 351 | 352 | 353 | matchobj_t ctrlp_find_match(PyObject* str, PyObject* abbrev, char *mmode) 354 | { 355 | long i, max; 356 | double score; 357 | matchobj_t returnobj; 358 | 359 | // Make a copy of input string to replace all backslashes. 360 | // We need to create a copy because PyString_AsString returns 361 | // string that must not be changed. 362 | // We will free() it later 363 | char *temp_string; 364 | temp_string = strduplicate(PyString_AsString(str)); 365 | 366 | // Replace all backslashes 367 | for (i = 0; i < strlen(temp_string); i++) { 368 | if (temp_string[i] == '\\') { 369 | temp_string[i] = '/'; 370 | } 371 | } 372 | 373 | matchinfo_t m; 374 | if (strcmp(mmode, "filename-only") == 0) { 375 | // get file name by splitting string on slashes 376 | m.haystack_p = slashsplit(temp_string); 377 | m.haystack_len = strlen(m.haystack_p); 378 | } 379 | else { 380 | m.haystack_p = temp_string; 381 | m.haystack_len = PyString_Size(str); 382 | } 383 | m.needle_p = PyString_AsString(abbrev); 384 | m.needle_len = PyString_Size(abbrev); 385 | m.max_score_per_char = (1.0 / m.haystack_len + 1.0 / m.needle_len) / 2; 386 | m.dot_file = 0; 387 | 388 | // calculate score 389 | score = 1.0; 390 | 391 | // special case for zero-length search string 392 | if (m.needle_len == 0) { 393 | 394 | // filter out dot files 395 | for (i = 0; i < m.haystack_len; i++) { 396 | char c = m.haystack_p[i]; 397 | if (c == '.' && (i == 0 || m.haystack_p[i - 1] == '/')) { 398 | score = 0.0; 399 | break; 400 | } 401 | } 402 | } else if (m.haystack_len > 0) { // normal case 403 | 404 | // prepare for memoization 405 | double memo[m.haystack_len * m.needle_len]; 406 | for (i = 0, max = m.haystack_len * m.needle_len; i < max; i++) 407 | memo[i] = DBL_MAX; 408 | m.memo = memo; 409 | 410 | score = ctrlp_recursive_match(&m, 0, 0, 0, 0.0); 411 | } 412 | 413 | // need to free memory because strdump() function in slashsplit() uses 414 | // malloc to allocate memory, otherwise memory will leak 415 | if (strcmp(mmode, "filename-only") == 0) { 416 | free(m.haystack_p); 417 | } 418 | 419 | // Free memory after strdup() 420 | free(temp_string); 421 | 422 | returnobj.str = str; 423 | returnobj.score = score; 424 | 425 | return returnobj; 426 | } 427 | 428 | static PyMethodDef fuzzycomt_funcs[] = { 429 | {"match", (PyCFunction)ctrlp_fuzzycomt_match, METH_NOARGS, NULL}, 430 | { "match", ctrlp_fuzzycomt_match, METH_VARARGS, NULL }, 431 | {"sorted_match_list", (PyCFunction)ctrlp_fuzzycomt_sorted_match_list, METH_NOARGS, NULL}, 432 | { "sorted_match_list", ctrlp_fuzzycomt_sorted_match_list, METH_VARARGS, NULL }, 433 | {NULL} 434 | }; 435 | 436 | PyMODINIT_FUNC initfuzzycomt() 437 | { 438 | Py_InitModule3("fuzzycomt", fuzzycomt_funcs, 439 | "Fuzzy matching module"); 440 | } 441 | -------------------------------------------------------------------------------- /autoload/fuzzycomt.h: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Stanislav Golovanov 2 | // Matching algorithm by Wincent Colaiuta, 2010. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, 10 | // this list of conditions and the following disclaimer in the documentation 11 | // and/or other materials provided with the distribution. 12 | // 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 17 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | // POSSIBILITY OF SUCH DAMAGE. 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | typedef struct { 32 | PyObject *str; // Python object with file path 33 | double score; // score of string 34 | } matchobj_t; 35 | 36 | typedef struct { 37 | char *haystack_p; // pointer to string to be searched 38 | long haystack_len; // length of same 39 | char *needle_p; // pointer to search string (abbreviation) 40 | long needle_len; // length of same 41 | double max_score_per_char; 42 | int dot_file; // boolean: true if str is a dot-file 43 | double *memo; // memoization 44 | } matchinfo_t; 45 | 46 | matchobj_t ctrlp_find_match(PyObject* str, PyObject* abbrev, char *mmode); 47 | 48 | void ctrlp_get_line_matches(PyObject* paths, PyObject* abbrev, matchobj_t matches[], char *mode); 49 | 50 | PyObject* ctrlp_fuzzycomt_match(PyObject* self, PyObject* args); 51 | 52 | PyObject* ctrlp_fuzzycomt_sorted_match_list(PyObject* self, PyObject* args); 53 | -------------------------------------------------------------------------------- /autoload/matcher.vim: -------------------------------------------------------------------------------- 1 | " CtrlP C matching extension 2 | " 3 | " By: Stanislav Golovanov 4 | " MaxSt 5 | " Aaron Jensen 6 | " 7 | " See LICENSE for licensing concerns. 8 | 9 | 10 | " Use pyeval() or py3eval() for newer python versions or fall back to 11 | " vim.command() if vim version is old 12 | " This code is borrowed from Powerline 13 | let s:matcher_pycmd = has('python') ? 'py' : 'py3' 14 | let s:matcher_pyeval = s:matcher_pycmd.'eval' 15 | 16 | if exists('*'. s:matcher_pyeval) 17 | let s:pyeval = function(s:matcher_pyeval) 18 | else 19 | exec s:matcher_pycmd 'import json, vim' 20 | exec "function! s:pyeval(e)\n". 21 | \ s:matcher_pycmd." vim.command('return ' + json.dumps(eval(vim.eval('a:e'))))\n". 22 | \"endfunction" 23 | endif 24 | 25 | let s:script_folder_path = escape( expand( ':p:h' ), '\' ) 26 | python << ImportEOF 27 | import sys, os, vim 28 | sys.path.insert( 0, os.path.abspath( vim.eval('s:script_folder_path' ) ) ) 29 | import fuzzycomt 30 | sys.path.pop(0) 31 | ImportEOF 32 | 33 | fu! s:matchtabs(item, pat) 34 | return match(split(a:item, '\t\+')[0], a:pat) 35 | endf 36 | 37 | fu! s:matchfname(item, pat) 38 | let parts = split(a:item, '[\/]\ze[^\/]\+$') 39 | return match(parts[-1], a:pat) 40 | endf 41 | 42 | fu! s:cmatcher(lines, input, limit, mmode, ispath, crfile) 43 | python << EOF 44 | lines = vim.eval('a:lines') 45 | searchinp = vim.eval('a:input') 46 | limit = int(vim.eval('a:limit')) 47 | mmode = vim.eval('a:mmode') 48 | ispath = int(vim.eval('a:ispath')) 49 | crfile = vim.eval('a:crfile') 50 | 51 | if ispath and crfile: 52 | try: 53 | lines.remove(crfile) 54 | except ValueError: 55 | pass 56 | 57 | try: 58 | # TODO we should support smartcase. Needs some fixing on matching side 59 | matchlist = fuzzycomt.sorted_match_list(lines, searchinp.lower(), limit, mmode) 60 | except: 61 | matchlist = [] 62 | EOF 63 | return s:pyeval("matchlist") 64 | endf 65 | 66 | fu! s:escapechars(chars) 67 | if exists('+ssl') && !&ssl 68 | cal map(a:chars, 'escape(v:val, ''\'')') 69 | en 70 | for each in ['^', '$', '.'] 71 | cal map(a:chars, 'escape(v:val, each)') 72 | endfo 73 | 74 | return a:chars 75 | endfu 76 | 77 | fu! s:highlight(input, mmode, regex) 78 | " highlight matches 79 | cal clearmatches() 80 | if a:regex 81 | let pat = "" 82 | if a:mmode == "filename-only" 83 | let pat = substitute(a:input, '\$\@ \\zs', 'g') 87 | en 88 | cal matchadd('CtrlPMatch', '\c'.pat) 89 | el 90 | let chars = split(a:input, '\zs') 91 | let chars = s:escapechars(chars) 92 | 93 | " Build a pattern like /a.*b.*c/ from abc (but with .\{-} non-greedy 94 | " matchers instead) 95 | let pat = join(chars, '.\{-}') 96 | " Ensure we match the last version of our pattern 97 | let ending = '\(.*'.pat.'\)\@!' 98 | " Case insensitive 99 | let beginning = '\c^.*' 100 | if a:mmode == "filename-only" 101 | " Make sure there are no slashes in our match 102 | let beginning = beginning.'\([^\/]*$\)\@=' 103 | end 104 | 105 | for i in range(len(a:input)) 106 | " Surround our current target letter with \zs and \ze so it only 107 | " actually matches that one letter, but has all preceding and trailing 108 | " letters as well. 109 | " \zsa.*b.*c 110 | " a\(\zsb\|.*\zsb)\ze.*c 111 | let charcopy = copy(chars) 112 | if i == 0 113 | let charcopy[i] = '\zs'.charcopy[i].'\ze' 114 | let middle = join(charcopy, '.\{-}') 115 | else 116 | let before = join(charcopy[0:i-1], '.\{-}') 117 | let after = join(charcopy[i+1:-1], '.\{-}') 118 | let c = charcopy[i] 119 | " for abc, match either ab.\{-}c or a.*b.\{-}c in that order 120 | let cpat = '\(\zs'.c.'\|'.'.*\zs'.c.'\)\ze.*' 121 | let middle = before.cpat.after 122 | endif 123 | 124 | " Now we matchadd for each letter, the basic form being: 125 | " ^.*\zsx\ze.*$, but with our pattern we built above for the letter, 126 | " and a negative lookahead ensuring that we only highlight the last 127 | " occurrence of our letters. We also ensure that our matcher is case 128 | " insensitive. 129 | cal matchadd('CtrlPMatch', beginning.middle.ending) 130 | endfor 131 | en 132 | cal matchadd('CtrlPLinePre', '^>') 133 | endf 134 | 135 | fu! matcher#cmatch(lines, input, limit, mmode, ispath, crfile, regex) 136 | if a:input == '' 137 | " Clear matches, that left from previous matches 138 | cal clearmatches() 139 | " Hack to clear s:savestr flag in SplitPattern, otherwise matching in 140 | " 'tag' mode will work only from 2nd char. 141 | cal ctrlp#call('s:SplitPattern', '') 142 | let array = a:lines[0:a:limit] 143 | if a:ispath && !empty(a:crfile) 144 | cal remove(array, index(array, a:crfile)) 145 | en 146 | return array 147 | el 148 | if a:regex 149 | let array = [] 150 | let func = a:mmode == "filename-only" ? 's:matchfname' : 'match' 151 | for item in a:lines 152 | if call(func, [item, a:input]) >= 0 153 | cal add(array, item) 154 | endif 155 | endfor 156 | cal sort(array, ctrlp#call('s:mixedsort')) 157 | cal s:highlight(a:input, a:mmode, a:regex) 158 | return array 159 | endif 160 | " use built-in matcher if mmode set to match until first tab ( in other case 161 | " tag.vim doesnt work 162 | if a:mmode == "first-non-tab" 163 | let array = [] 164 | " call ctrlp.vim function to get proper input pattern 165 | let pat = ctrlp#call('s:SplitPattern', a:input) 166 | for item in a:lines 167 | if call('s:matchtabs', [item, pat]) >= 0 168 | cal add(array, item) 169 | en 170 | endfo 171 | "TODO add highlight 172 | cal sort(array, ctrlp#call('s:mixedsort')) 173 | return array 174 | en 175 | 176 | let matchlist = s:cmatcher(a:lines, a:input, a:limit, a:mmode, a:ispath, a:crfile) 177 | en 178 | 179 | cal s:highlight(a:input, a:mmode, a:regex) 180 | 181 | return matchlist 182 | endf 183 | -------------------------------------------------------------------------------- /autoload/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | import os, platform 3 | 4 | if os.name == 'nt' and platform.architecture()[0] == '64bit': 5 | extSearch = Extension('fuzzycomt', ['fuzzycomt.c'], extra_compile_args=['-D MS_WIN64']) 6 | else: 7 | extSearch = Extension('fuzzycomt', ['fuzzycomt.c']) 8 | 9 | 10 | 11 | setup (name = 'fuzzycomt', 12 | version = '0.1', 13 | description = 'Fuzzy search in strings', 14 | ext_modules = [extSearch]) 15 | 16 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | chkPython2() 4 | { 5 | cmd=$1 6 | ret=$($cmd -V 2>&1) 7 | case "$ret" in 8 | "Python 2."*) 9 | return 0 10 | ;; 11 | *) 12 | return 1 13 | ;; 14 | esac 15 | } 16 | 17 | findPython2() 18 | { 19 | cmd_list="python python2 python27 python2.7 python26 python2.6" 20 | for cmd in $cmd_list; do 21 | if chkPython2 $cmd; then 22 | found_python=$cmd 23 | break 24 | fi 25 | done 26 | 27 | if [ "$found_python" = "" ]; then 28 | echo "cannot find python2 automatically" >&2 29 | while true; do 30 | read -p "please input your python 2 command: " cmd 31 | if chkPython2 "$cmd"; then 32 | found_python=$cmd 33 | break 34 | fi 35 | echo "verify [$cmd] with -V failed" >&2 36 | done 37 | fi 38 | 39 | echo $found_python 40 | } 41 | 42 | python=$(findPython2) 43 | echo "find python2 -> $python" 44 | 45 | cd autoload 46 | $python setup.py build 47 | cp build/lib*/fuzzycomt.so . 48 | -------------------------------------------------------------------------------- /install_windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pushd autoload 3 | python setup.py build -c mingw32 4 | pushd build\lib* 5 | xcopy fuzzycomt.pyd ..\..\ 6 | popd 7 | popd --------------------------------------------------------------------------------