├── preview.png ├── plugin └── finder.vim ├── autoload ├── finder │ ├── lines.vim │ ├── matches.vim │ ├── splitted.vim │ ├── tags.vim │ └── files.vim └── finder.vim ├── LICENSE └── README.md /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysoloviov/vim-finder/HEAD/preview.png -------------------------------------------------------------------------------- /plugin/finder.vim: -------------------------------------------------------------------------------- 1 | if(v:version < 800) 2 | echo "Finder requires Vim 8 or above." 3 | endif 4 | 5 | command! -nargs=* -complete=file Files call s:files() 6 | command! Lines call finder#lines() 7 | command! Tags call finder#tags() 8 | command! -nargs=1 Matches call finder#matches() 9 | 10 | function! s:files(...) 11 | if(a:0 == 0) 12 | call finder#files() 13 | elseif(a:0 == 1) 14 | call finder#files({"directory": a:1}) 15 | elseif(a:0 == 2) 16 | call finder#files({"directory": a:1, "openBufferCommand": a:2}) 17 | endif 18 | endfunction 19 | -------------------------------------------------------------------------------- /autoload/finder/lines.vim: -------------------------------------------------------------------------------- 1 | function! finder#lines#index(...) 2 | let options = get(a:, 1, {}) 3 | let options.bufferName = get(options, "bufferName", "lines") 4 | let options.prompt = get(options, "prompt", "Lines> ") 5 | 6 | let lines = getline(0, "$") 7 | 8 | if(len(lines) == 1 && lines[0] == "") 9 | return finder#error("Buffer is empty.") 10 | endif 11 | 12 | let items = [] 13 | let lineNumber = 0 14 | 15 | for line in lines 16 | let lineNumber += 1 17 | let item = {} 18 | let item.raw = line 19 | let item.position = [lineNumber, -1] 20 | 21 | call add(items, item) 22 | endfor 23 | 24 | call finder#splitted(items, options) 25 | endfunction 26 | -------------------------------------------------------------------------------- /autoload/finder/matches.vim: -------------------------------------------------------------------------------- 1 | function! finder#matches#index(pattern, ...) 2 | let options = get(a:, 1, {}) 3 | let options.bufferName = get(options, "bufferName", "matches") 4 | let options.prompt = get(options, "prompt", "Matches> ") 5 | let options.header = get(options, "header", s:getHeader(a:pattern)) 6 | 7 | let offsetType = get(options, "offsetType", 2) 8 | let lines = getline(0, "$") 9 | let items = [] 10 | let lineNumber = 0 11 | 12 | for line in lines 13 | let matchesInString = finder#getStringMatches(line, a:pattern, offsetType) 14 | let lineNumber += 1 15 | 16 | for m in matchesInString 17 | let item = {} 18 | let item.raw = m[0] 19 | let item.position = [lineNumber, m[1] + 1] 20 | 21 | call add(items, item) 22 | endfor 23 | endfor 24 | 25 | if(len(items) == 0) 26 | return finder#error("No matches were found.") 27 | endif 28 | 29 | call finder#splitted(items, options) 30 | endfunction 31 | 32 | function! s:getHeader(pattern) 33 | let header = finder#getHeader() 34 | 35 | call insert(header, " Pattern: " . a:pattern, 2) 36 | 37 | return header 38 | endfunction 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Evgeniy Solovyov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autoload/finder/splitted.vim: -------------------------------------------------------------------------------- 1 | function! finder#splitted#index(items, ...) 2 | let options = get(a:, 1, {}) 3 | let options.openBufferCommand = get(options, "openBufferCommand", "vnew") 4 | let options.allowedOpenBufferCommands = ["new", "vnew"] 5 | let options.handler = get(options, "handler", function("s:handler")) 6 | 7 | let previousWindowView = winsaveview() 8 | let showPreview = get(options, "showPreview", 1) 9 | 10 | call finder#init(a:items, options) 11 | 12 | if(has_key(options, "windowSize")) 13 | execute "vertical resize " . options.windowSize 14 | endif 15 | 16 | call finder#fill() 17 | 18 | execute printf("autocmd User FinderCancelled wincmd w | call winrestview(%s) | wincmd w", string(previousWindowView)) 19 | 20 | if(showPreview) 21 | autocmd User FinderItemSelected call preview(finder#getSelectedItem()) 22 | endif 23 | endfunction 24 | 25 | function! s:handler(item) 26 | execute printf("%iwincmd w", b:previousWindow) 27 | 28 | call s:moveCursor(a:item.position[0], a:item.position[1]) 29 | endfunction 30 | 31 | function! s:preview(item) 32 | let currentWindow = b:currentWindow 33 | execute printf("%iwincmd w", b:previousWindow) 34 | 35 | call s:moveCursor(a:item.position[0], a:item.position[1]) 36 | 37 | set cursorline! 38 | set cursorline! 39 | execute printf("%iwincmd w", currentWindow) 40 | endfunction 41 | 42 | function! s:moveCursor(line, column) 43 | if(a:column == -1) 44 | call cursor(a:line, 0) 45 | normal ^ 46 | else 47 | call cursor(a:line, a:column) 48 | endif 49 | 50 | normal zzl 51 | endfunction 52 | -------------------------------------------------------------------------------- /autoload/finder/tags.vim: -------------------------------------------------------------------------------- 1 | let s:MIN_COLUMNS = 25 2 | 3 | function! finder#tags#index(...) 4 | if(!executable("ctags")) 5 | return finder#error("Ctags was not found in your $PATH.") 6 | endif 7 | 8 | if(!filereadable(expand("%"))) 9 | return finder#error("File cannot be read.") 10 | endif 11 | 12 | if(&ft == "") 13 | return finder#error("Please specify filetype.") 14 | endif 15 | 16 | let options = get(a:, 1, {}) 17 | let options.bufferName = get(options, "bufferName", "tags") 18 | let options.prompt = get(options, "prompt", "Tags> ") 19 | let options.header = get(options, "header", []) 20 | 21 | let command = get(options, "command", "ctags -x --sort=no --format=2 --language-force=%s %s") 22 | let tags = systemlist(printf(command, &ft, shellescape(expand("%")))) 23 | 24 | if(v:shell_error || len(tags) == 0) 25 | return finder#error("No tags were found.") 26 | endif 27 | 28 | let maxTagLength = 0 29 | 30 | " find max tag length 31 | for tag in tags 32 | let parts = split(tag, '\s\+') 33 | 34 | if(len(parts[0]) > maxTagLength) 35 | let maxTagLength = len(parts[0]) 36 | endif 37 | endfor 38 | 39 | let columns = max([s:MIN_COLUMNS, maxTagLength + max([len(options.prompt) + 1, 5])]) 40 | let windowSize = get(options, "windowSize", columns) 41 | let options.windowSize = min([windowSize, winwidth(0) / 2]) 42 | let items = [] 43 | 44 | for tag in tags 45 | let parts = split(tag, '\s\+') 46 | let spaceDiff = columns - len(parts[0]) 47 | 48 | let item = {} 49 | let item.raw = parts[0] 50 | let item.comment = repeat(" ", spaceDiff - 1) . parts[1][0] 51 | let item.position = [parts[2], -1] 52 | 53 | call add(items, item) 54 | endfor 55 | 56 | call finder#splitted(items, options) 57 | endfunction 58 | -------------------------------------------------------------------------------- /autoload/finder/files.vim: -------------------------------------------------------------------------------- 1 | let s:BASENAME_SYNTAX_MATCH_REGION = '\(^\|\%(\/\)\@<=\)[^\/]\+' 2 | let s:HEADER_PLACEHOLDER_MODE = "{mode}" 3 | let s:HEADER_IGNORED_LABEL = "Ignored" 4 | let s:HEADER_IGNORED_PATH_SEPARATOR = ", " 5 | 6 | function! finder#files#index(...) 7 | let options = get(a:, 1, {}) 8 | let options.bufferName = get(options, "bufferName", "files") 9 | let options.prompt = get(options, "prompt", "Files> ") 10 | let options.handler = get(options, "handler", function("s:handler")) 11 | let options.startWith = get(options, "startWith", "^") 12 | let options.comparator = get(options, "comparator", function("finder#comparator")) 13 | let options.headerContainedGroups = get(options, "headerContainedGroups", ["finderFilesHeaderIgnoredPaths"]) 14 | 15 | let directory = get(options, "directory", ".") 16 | let command = get(options, "command", "find %s -type f") 17 | let ignoredPaths = get(options, "ignoredPaths", ["*/.git/*", "*/.svn/*"]) 18 | let baseName = get(options, "baseName", 1) 19 | let options.header = get(options, "header", s:getHeader(directory, ignoredPaths)) 20 | 21 | if(!isdirectory(fnamemodify(directory, ":p"))) 22 | return finder#error("Directory is not exist.") 23 | endif 24 | 25 | " assign directory 26 | let command = printf(command, directory) 27 | 28 | for path in ignoredPaths 29 | let command .= printf(" ! -path '%s'", path) 30 | endfor 31 | 32 | " remove ./ at the beginning of the line 33 | let command .= ' | sed "s|^\./||"' 34 | 35 | let paths = systemlist(command) 36 | let baseNames = [] 37 | let options.raw = baseName ? baseNames : paths 38 | 39 | if(len(paths) == 0) 40 | return finder#error("There are no files in this directory.") 41 | endif 42 | 43 | let items = [] 44 | 45 | for path in paths 46 | let item = {} 47 | let item.raw = path 48 | 49 | call add(items, item) 50 | call add(baseNames, fnamemodify(path, ":t")) 51 | endfor 52 | 53 | call finder#init(items, options) 54 | call s:defineSyntax() 55 | call s:defineHighlighting() 56 | 57 | let b:baseName = baseName 58 | let b:paths = paths 59 | let b:baseNames = baseNames 60 | 61 | inoremap =finder#call("toggleBaseName") 62 | 63 | if(b:baseName) 64 | call s:enableBaseName() 65 | else 66 | call s:disableBaseName() 67 | endif 68 | 69 | call finder#fill() 70 | endfunction 71 | 72 | function! s:handler(item) 73 | execute printf("silent edit %s", fnameescape(a:item.raw)) 74 | endfunction 75 | 76 | function! s:toggleBaseName() 77 | if(b:baseName) 78 | call s:disableBaseName() 79 | else 80 | call s:enableBaseName() 81 | endif 82 | 83 | call timer_start(0, "finder#redraw") 84 | endfunction 85 | 86 | function! s:enableBaseName() 87 | let b:raw = b:baseNames 88 | let b:mode = "basename" 89 | let b:baseName = 1 90 | 91 | call finder#setSyntaxMatchRegion(s:BASENAME_SYNTAX_MATCH_REGION) 92 | endfunction 93 | 94 | function! s:disableBaseName() 95 | let b:raw = b:paths 96 | let b:mode = "default" 97 | let b:baseName = 0 98 | 99 | call finder#setSyntaxMatchRegion(finder#get("SYNTAX_MATCH_REGION")) 100 | endfunction 101 | 102 | function! s:getHeader(directory, ignoredPaths) 103 | let header = finder#getHeader() 104 | let directory = fnamemodify(a:directory, ":~") 105 | 106 | call insert(header, ' Directory: ' . directory, 2) 107 | call insert(header, printf(" %s: %s", s:HEADER_IGNORED_LABEL, join(a:ignoredPaths, s:HEADER_IGNORED_PATH_SEPARATOR)), 3) 108 | call insert(header, ' Mode: ' . s:HEADER_PLACEHOLDER_MODE, 4) 109 | 110 | return header 111 | endfunction 112 | 113 | function! s:defineSyntax() 114 | execute printf('syntax match finderFilesHeaderIgnoredPaths /\%%(%s:\)\@<=.*/ contained contains=finderFilesHeaderIgnoredPath,finderFilesHeaderIgnoredPathSeparator', s:HEADER_IGNORED_LABEL) 115 | execute printf('syntax match finderFilesHeaderIgnoredPath /.\{-}\ze\(%s\|$\)/ contained', s:HEADER_IGNORED_PATH_SEPARATOR) 116 | execute printf('syntax match finderFilesHeaderIgnoredPathSeparator /%s\ze/ contained', s:HEADER_IGNORED_PATH_SEPARATOR) 117 | endfunction 118 | 119 | function! s:defineHighlighting() 120 | hi link finderFilesHeaderIgnoredPath finderHeaderValue 121 | hi link finderFilesHeaderIgnoredPathSeparator finderHeaderValue 122 | endfunction 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![preview](https://raw.githubusercontent.com/damage220/vim-finder/master/preview.png) 2 | 3 | Finder 4 | ------ 5 | Vim plugin to search files, tags, lines and matches 6 | ([demonstration](https://www.youtube.com/watch?v=TvBJhOOSlOc)). 7 | It also provides an API to build your own extensions. 8 | 9 | Dependencies 10 | ------------ 11 | - Vim 8.0 12 | - Exuberant ctags 13 | - Unix find, grep tools 14 | 15 | Installation 16 | ------------ 17 | It is recommended to use a plugin manager like [vim-plug](https://github.com/junegunn/vim-plug) or others. 18 | 19 | ```vim 20 | Plug 'damage220/vim-finder' 21 | ``` 22 | 23 | Synopsis 24 | -------- 25 | Function | Command | Description 26 | --- | --- | --- 27 | `finder#files([options])` | `Files [directory] [openBufferCommand]` | List files. Be careful, by default, files are shown in the current buffer. See `openBufferCommand` below. 28 | `finder#tags([options])` | `Tags` | List buffer tags. 29 | `finder#lines([options])` | `Lines` | List buffer lines. 30 | `finder#matches(pattern, [options])` | `Matches pattern` | List buffer matches. 31 | 32 | Where `pattern` is the vim pattern (:help pattern) and `options` is an optional 33 | `Dictionary`. Here is common and mode-specific options. 34 | 35 | #### options 36 | 37 | Option | Type | Default | Description 38 | --- | --- | --- | --- 39 | `openBufferCommand` | String | enew | Is used to open new buffer. One of the list `[enew, new, vnew, tabe]` 40 | `bufferName` | String | finder | Title for the tabline. 41 | `prompt` | String | > | Text is shown near the query. 42 | `startWith` | String | | Prepend query with given string. 43 | `handler` | Funcref | function("finder#handler") | Do some job with selected item. 44 | `limit` | Number | 100 | Maximum amount of occurrences. 45 | `finder` | Funcref | function("finder#getMatches") | Returns List of matched items. 46 | `header` | List | see code | Block of text shown at the top of the buffer. To hide, pass empty List. 47 | `comparator` | Funcref | v:null | Is passed to vim `sort` function. To disable sorting, pass non-Funcref value. 48 | `grepCommand` | String | grep -ni -m %i %%s | GNU grep pattern used for searching. Used for default `finder`. 49 | `headerContainedGroups` | List | [] | List of additional contained syntax groups in the `header`. Useful when you want to define custom syntax highlighting for the `header`. 50 | `syntaxMatchRegion` | String | ^.\+ | Syntax region where matches should be highlighted. For instance, useful to restrict the highlighting only for basename part. 51 | 52 | #### finder#files 53 | 54 | Option | Type | Default | Description 55 | --- | --- | --- | --- 56 | `directory` | String | . | Directory to search in. 57 | `command` | String | find %s -type f | Unix find pattern. 58 | `ignoredPaths` | List | ["\*/.git/\*", "\*/.svn/\*"] | List of strings are passed to `! -path` flag. 59 | `baseName` | Boolean | 1 | Whether or not use basename instead of full path. 60 | 61 | **Note:** there are no shortcuts like `` and `` to open file in 62 | splitted window. You should firstly open one with `:new` or `:vnew` command 63 | and then execute `:Files` command. It is also possible to execute `:Files . vnew` 64 | command. To toggle basename mode use ``. 65 | 66 | #### finder#tags 67 | 68 | Option | Type | Default | Description 69 | --- | --- | --- | --- 70 | `command` | String | ctags -x --sort=no --format=2 --language-force=%s %s | Exuberant ctags pattern. 71 | `showPreview` | Boolean | 1 | Focus tag while navigating. 72 | 73 | #### finder#lines 74 | 75 | Option | Type | Default | Description 76 | --- | --- | --- | --- 77 | `showPreview` | Boolean | 1 | Focus line while navigating. 78 | 79 | #### finder#matches 80 | 81 | Option | Type | Default | Description 82 | --- | --- | --- | --- 83 | `offsetType` | Number | 2 | Specify from which position the next match should be looked for in the string. 1 - after the start of previous match, 2 - after the end. 84 | `showPreview` | Boolean | 1 | Focus match while navigating. 85 | 86 | #### Examples 87 | 88 | ```vim 89 | :call finder#files({"openBufferCommand": "tabe"}) 90 | :call finder#matches('function! \zs.\{-}\ze(') 91 | ``` 92 | 93 | Syntax 94 | ------ 95 | Group | 96 | --- | 97 | `finderHeader` | 98 | `finderHeaderLabel` | 99 | `finderHeaderValue` | 100 | `finderHeaderShortcut` | 101 | `finderHeaderShortcutSeparator` | 102 | `finderHeaderShortcutNote` | 103 | `finderFilesHeaderIgnoredPaths` | 104 | `finderFilesHeaderIgnoredPath` | 105 | `finderFilesHeaderIgnoredPathSeparator` | 106 | `finderPrompt` | 107 | `finderQuery` | 108 | `finderBody` | 109 | `finderSelected` | 110 | `finderComment` | 111 | `finderSelectedComment` | 112 | `finderMatch` | 113 | `finderSelectedMatch` | 114 | 115 | Extending 116 | --------- 117 | You are free to add your own extensions. To do this, simply call 118 | `finder#custom(items, [options])` or `finder#splitted(items, [options])` 119 | function. The first one is the base that all functions are "inherited" from. 120 | The second is useful when you work with the data in the buffer and, probably, 121 | want to see a preview while navigating. `items` is a List of Dictionaries. 122 | Dictionary should contain at least `raw` key. `finder#splitted` also requires 123 | `item.position` that is a List of two elements: line and column where item 124 | is placed in the buffer. 125 | 126 | #### items 127 | Key | Type | Required | Default | Description 128 | --- | --- | --- | --- | --- 129 | `raw` | String | Yes | | Used to make search. 130 | `visibleLine` | String | No | `raw` | Used to show in the buffer. 131 | `comment` | String | No | | Used to show to the right of the `visibleLine`. 132 | 133 | #### Events 134 | Event | Description 135 | --- | --- 136 | `FinderCancelled` | Triggered when `` has been pressed. 137 | `FinderItemSelected` | Triggered when new item has been selected. 138 | 139 | #### Example 140 | 141 | ```vim 142 | function! ListFiles(...) 143 | let options = get(a:, 1, {}) 144 | let options.prompt = "Files> " 145 | let options.openBufferCommand = "tabe" 146 | let options.handler = function("OpenFile") 147 | 148 | let items = [] 149 | let files = systemlist("find . -type f") 150 | 151 | if(len(files) == 0) 152 | return finder#error("There are no files in this directory.") 153 | endif 154 | 155 | for file in files 156 | let item = {} 157 | let item.raw = file 158 | " You probably want to add something else to this dictionary. 159 | 160 | call add(items, item) 161 | endfor 162 | 163 | call finder#custom(items, options) 164 | endfunction 165 | 166 | function! OpenFile(item) 167 | execute printf("edit %s", fnameescape(a:item.raw)) 168 | endfunction 169 | ``` 170 | 171 | When `` or `` has been pressed, `OpenFile` function will be called 172 | and the selected item will be passed to the `handler`. 173 | 174 | #### header 175 | 176 | ```vim 177 | function! ListFiles(...) 178 | let options = get(a:, 1, {}) 179 | let options.header = [ 180 | \ repeat("=", 17), 181 | \ "Foobar: {foobar}", 182 | \ repeat("=", 17), 183 | \ ] 184 | 185 | " create items list 186 | let items ... 187 | 188 | call finder#init(items, options) 189 | 190 | let b:foobar = "some value" 191 | 192 | call finder#fill() 193 | endfunction 194 | ``` 195 | 196 | String wrapped with {} is a variable. `Finder` parses all strings in `header`, 197 | find variables and fill them with appropriate buffer variables. That is why in 198 | this example we do not use `finder#custom` function, because it also includes 199 | header rendering and since there is no `b:foobar` variable we need firstly 200 | create a buffer, assign variable and then call `finder#fill` function. 201 | Then you can change `b:foobar` as much as you want. Do not forget to call 202 | `finder#updateHeader` after resetting value. Of course, if you need not complex 203 | logic it is more rational to use static text. You also can overload 204 | `finder#getHeader` function in your `.vimrc` to apply new header globally. 205 | 206 | Default header uses `b:matchesAmount` and `b:itemsAmount`. You can use them too 207 | and all other buffer variables defined by default. You can list all buffer 208 | variables typing `:echo b:` and pressing ``. Of course, you can simply 209 | open the source code. 210 | 211 | License 212 | ------- 213 | MIT License 214 | -------------------------------------------------------------------------------- /autoload/finder.vim: -------------------------------------------------------------------------------- 1 | let s:NAME = "Precise Finder" 2 | let s:BUFFER_NAME = "finder" 3 | let s:ALLOWED_OPEN_BUFFER_COMMANDS = ["enew", "new", "vnew", "tabe"] 4 | let s:LIMIT = 100 5 | let s:HEADER_TITLE = 0 6 | let s:HEADER_MATCHES = 1 7 | let s:HEADER_MAPPINGS = 2 8 | let s:HEADER_PLACEHOLDER_MATCHES_AMOUNT = "{matchesAmount}" 9 | let s:HEADER_PLACEHOLDER_ITEMS_AMOUNT = "{itemsAmount}" 10 | let s:HEADER_PLACEHOLDER_PATTERN = '{%s}' 11 | let s:HEADER_PARAMETER_PATTERN = '{\zs\w\+\ze}' 12 | let s:HEADER_QUICK_HELP_LABEL = "Quick Help" 13 | let s:TAG_LINE_END = "#finderendline" 14 | let s:SYNTAX_MATCH_REGION = '^.\+' 15 | let s:DEFAULT_GLOBAL_OPTIONS = {} 16 | let s:GLOBAL_OPTIONS = { 17 | \ "showmode": 0, 18 | \ "rulerformat": "%0(%)", 19 | \ "updatetime": 999999999, 20 | \ "number": 0, 21 | \ "relativenumber": 0, 22 | \ "laststatus": 0, 23 | \ } 24 | 25 | function! finder#files(...) 26 | call call("finder#files#index", a:000) 27 | endfunction 28 | 29 | function! finder#tags(...) 30 | call call("finder#tags#index", a:000) 31 | endfunction 32 | 33 | function! finder#lines(...) 34 | call call("finder#lines#index", a:000) 35 | endfunction 36 | 37 | function! finder#matches(...) 38 | call call("finder#matches#index", a:000) 39 | endfunction 40 | 41 | function! finder#splitted(...) 42 | call call("finder#splitted#index", a:000) 43 | endfunction 44 | 45 | function! finder#custom(items, ...) 46 | call call("finder#init", [a:items, get(a:, 1, {})]) 47 | call finder#fill() 48 | endfunction 49 | 50 | function! finder#init(items, ...) 51 | if(len(a:items) == 0) 52 | return finder#error("Nothing to match.") 53 | endif 54 | 55 | let options = get(a:, 1, {}) 56 | let openBufferCommand = get(options, "openBufferCommand", "enew") 57 | let bufferName = get(options, "bufferName", s:BUFFER_NAME) 58 | let allowedOpenBufferCommands = get(options, "allowedOpenBufferCommands", s:ALLOWED_OPEN_BUFFER_COMMANDS) 59 | let previousWindow = winnr() 60 | 61 | if(index(allowedOpenBufferCommands, openBufferCommand) == -1) 62 | return finder#error(printf("Not allowed command. Allowed: %s", allowedOpenBufferCommands)) 63 | endif 64 | 65 | " set global options 66 | for [option, value] in items(s:GLOBAL_OPTIONS) 67 | let s:DEFAULT_GLOBAL_OPTIONS[option] = getbufvar("%", "&" . option) 68 | 69 | call setbufvar("%", "&" . option, value) 70 | endfor 71 | 72 | " need redraw to clear ruler 73 | redraw 74 | 75 | " creating a buffer 76 | execute openBufferCommand 77 | 78 | " post buffer commands 79 | execute printf("silent file %s", bufferName) 80 | mapclear 81 | 82 | " set local options 83 | setlocal filetype=finder 84 | setlocal buftype=nofile 85 | setlocal nocursorline 86 | setlocal conceallevel=3 87 | setlocal concealcursor=nvic 88 | setlocal nowrap 89 | 90 | " set buffer variables 91 | let b:previousWindow = previousWindow 92 | let b:currentWindow = winnr() 93 | let b:currentBuffer = bufnr("%") 94 | let b:items = finder#getCompleteItems(a:items) 95 | let b:itemsAmount = len(b:items) 96 | let b:raw = exists("options.raw") ? options.raw : finder#getRaw(b:items) 97 | let b:limit = get(options, "limit", s:LIMIT) 98 | let b:finder = get(options, "finder", function("finder#getMatches")) 99 | 100 | " used for scrolling 101 | let b:hiddenLines = [] 102 | 103 | let b:header = get(options, "header", finder#getHeader()) 104 | let b:headerVariables = finder#getHeaderVariables(b:header) 105 | let b:headerContainedGroups = get(options, "headerContainedGroups", []) 106 | let b:queryLine = len(b:header) + 1 107 | let b:firstMatchLine = b:queryLine + 1 108 | let b:hoveredLine = b:firstMatchLine 109 | let b:cancelled = 1 110 | let b:prompt = get(options, "prompt", "> ") 111 | let b:startWith = get(options, "startWith", "") 112 | let b:query = b:startWith 113 | let b:previousQuery = b:startWith 114 | let b:keyPressed = 0 115 | let b:matchesAmount = min([b:itemsAmount, b:limit]) 116 | let b:handler = get(options, "handler", function("finder#handler")) 117 | let b:comparator = get(options, "comparator", v:null) 118 | let b:grepCommand = printf("grep -ni -m %i %%s", b:limit) 119 | let b:queryStartColumn = len(b:prompt) + 1 120 | 121 | call finder#defineMappings() 122 | call finder#defineEventListeners() 123 | call finder#defineHighlighting() 124 | call finder#defineSyntax() 125 | call finder#setSyntaxMatchRegion(get(options, "syntaxMatchRegion", s:SYNTAX_MATCH_REGION)) 126 | endfunction 127 | 128 | function! finder#getCompleteItems(items) 129 | let key = 0 130 | 131 | for item in a:items 132 | let item.key = key 133 | let key += 1 134 | endfor 135 | 136 | if(!has_key(a:items[0], "visibleLine")) 137 | for item in a:items 138 | let item.visibleLine = item.raw 139 | endfor 140 | endif 141 | 142 | if(!has_key(a:items[0], "comment")) 143 | for item in a:items 144 | let item.comment = "" 145 | endfor 146 | endif 147 | 148 | return a:items 149 | endfunction 150 | 151 | function! finder#fill() 152 | " insert header 153 | call finder#updateHeader() 154 | 155 | " white space at the end is used to avoid cursor jerk 156 | call finder#setQueryLine(b:startWith . " ") 157 | 158 | call timer_start(0, "finder#redraw") 159 | call feedkeys("s") 160 | endfunction 161 | 162 | function! finder#defineEventListeners() 163 | autocmd TextChangedI call finder#textChangedI() 164 | autocmd InsertLeave call finder#insertLeave() 165 | endfunction 166 | 167 | function! finder#defineMappings() 168 | inoremap finder#canGoLeft() ? "\" : "" 169 | inoremap finder#canGoLeft() ? "\" : "" 170 | inoremap col(".") == col("$") ? "" : "\" 171 | inoremap finder#canGoLeft() ? "\" : "" 172 | inoremap =finder#call("cursor", b:queryLine, b:queryStartColumn) 173 | inoremap =finder#call("cursor", b:queryLine, col('$')) 174 | inoremap =finder#call("finder#setQueryLine", "") 175 | inoremap =finder#call("finder#selectNextItem") 176 | inoremap =finder#call("finder#selectPreviousItem") 177 | inoremap =finder#call("finder#selectNextItem") 178 | inoremap =finder#call("finder#selectPreviousItem") 179 | inoremap =finder#call("finder#selectNextItem") 180 | inoremap =finder#call("finder#selectPreviousItem") 181 | inoremap =finder#call("finder#handle") 182 | inoremap =finder#call("finder#handle") 183 | endfunction 184 | 185 | function! finder#call(fn, ...) 186 | call call(a:fn, a:000) 187 | 188 | return "" 189 | endfunction 190 | 191 | function! finder#defineSyntax() 192 | " header 193 | execute printf('syntax region finderHeader start=/\%%^/ end=/\%%%il$/ contains=finderHeaderLabel,finderHeaderValue,finderHeaderQuickHelp,%s', b:queryLine - 1, join(b:headerContainedGroups, ",")) 194 | syntax match finderHeaderLabel /^\s*\zs.\{-}\ze:/ contained 195 | syntax match finderHeaderValue /: \zs.\+/ contained 196 | execute printf('syntax match finderHeaderQuickHelp /\%%(%s:\)\@<=.*/ contained contains=finderHeaderShortcut,finderHeaderShortcutSeparator,finderHeaderShortcutNote', s:HEADER_QUICK_HELP_LABEL) 197 | syntax match finderHeaderShortcut /\S\+\ze:/ contained 198 | syntax match finderHeaderShortcutSeparator /:/ contained 199 | syntax match finderHeaderShortcutNote /[^:]\{-}\ze\( \|$\)/ contained 200 | 201 | " query line 202 | execute printf('syntax match finderQuery /\%%%il\%%%ic.*$/', b:queryLine, b:queryStartColumn) 203 | execute printf('syntax match finderPrompt /^\%%%il.\{%i\}/', b:queryLine, len(b:prompt)) 204 | 205 | " body 206 | execute printf('syntax region finderBody start=/\%%%il/ end=/\%%$/ contains=finderSelected,finderMatch,finderHidden', b:firstMatchLine) 207 | execute printf('syntax match finderComment /\(%s\)\@<=.*$/ contained', s:TAG_LINE_END) 208 | execute printf('syntax match finderSelectedComment /\(%s\)\@<=.*$/ contained', s:TAG_LINE_END) 209 | execute printf('syntax match finderHidden /%s/ conceal contained', s:TAG_LINE_END) 210 | endfunction 211 | 212 | function! finder#defineHighlighting() 213 | hi link finderHeader Comment 214 | hi finderHeaderLabel ctermfg=109 ctermbg=NONE cterm=NONE 215 | hi finderHeaderValue ctermfg=215 ctermbg=NONE cterm=NONE 216 | hi finderHeaderShortcut ctermfg=222 ctermbg=NONE cterm=NONE 217 | hi link finderHeaderShortcutSeparator finderHeader 218 | hi finderHeaderShortcutNote ctermfg=255 ctermbg=NONE cterm=NONE 219 | hi finderPrompt ctermfg=81 ctermbg=NONE cterm=NONE 220 | hi finderQuery ctermfg=255 ctermbg=NONE cterm=NONE 221 | hi finderBody ctermfg=109 ctermbg=NONE cterm=NONE 222 | hi finderSelected ctermfg=109 ctermbg=0 cterm=bold 223 | hi finderComment ctermfg=109 ctermbg=NONE cterm=NONE 224 | hi finderSelectedComment ctermfg=109 ctermbg=0 cterm=bold 225 | hi finderMatch ctermfg=215 ctermbg=NONE cterm=bold,underline 226 | hi finderSelectedMatch ctermfg=215 ctermbg=0 cterm=bold,underline 227 | endfunction 228 | 229 | function! finder#setSyntaxMatchRegion(region) 230 | let b:syntaxMatchRegion = printf('\c%s%s.*$\&.\{-}%%s%s', a:region, s:TAG_LINE_END, s:TAG_LINE_END) 231 | endfunction 232 | 233 | function! finder#insertLeave() 234 | if(b:cancelled) 235 | call finder#exciteUserEvent("FinderCancelled") 236 | call finder#leave(b:currentBuffer) 237 | endif 238 | endfunction 239 | 240 | function! finder#handle() 241 | let b:cancelled = 0 242 | let currentBuffer = b:currentBuffer 243 | let item = finder#getSelectedItem() 244 | 245 | if(type(item) != v:t_dict) 246 | return finder#error("Nothing to handle.") 247 | endif 248 | 249 | stopinsert 250 | let stay = call(b:handler, [item]) 251 | 252 | if(!stay) 253 | call finder#leave(currentBuffer) 254 | endif 255 | endfunction 256 | 257 | function! finder#leave(currentBuffer) 258 | for [option, value] in items(s:DEFAULT_GLOBAL_OPTIONS) 259 | call setbufvar("%", "&" . option, value) 260 | endfor 261 | 262 | autocmd! User 263 | execute "bdelete " . a:currentBuffer 264 | endfunction 265 | 266 | function! finder#textChangedI() 267 | let b:query = strpart(getline(b:queryLine), b:queryStartColumn - 1) 268 | 269 | if(b:query != b:previousQuery) 270 | call timer_start(0, "finder#redraw") 271 | endif 272 | endfunction 273 | 274 | function! finder#redraw(timer) 275 | let b:previousQuery = b:query 276 | let windowView = winsaveview() 277 | 278 | if(line('$') > b:queryLine) 279 | silent execute printf("%i,$d", b:firstMatchLine) 280 | endif 281 | 282 | call winrestview(windowView) 283 | 284 | let b:matches = call(b:finder, [b:items]) 285 | 286 | if(type(b:comparator) == v:t_func) 287 | call sort(b:matches, b:comparator) 288 | endif 289 | 290 | let b:matchesAmount = len(b:matches) 291 | 292 | call finder#updateHeader() 293 | 294 | let i = b:firstMatchLine 295 | 296 | for m in b:matches 297 | let line = m.visibleLine . s:TAG_LINE_END . m.comment 298 | 299 | call setline(i, line) 300 | 301 | let i += 1 302 | endfor 303 | 304 | call finder#updateSyntax(b:query) 305 | 306 | if(b:matchesAmount > 0) 307 | call finder#hoverLine(b:firstMatchLine) 308 | endif 309 | 310 | let b:hiddenLines = [] 311 | let b:keyPressed = 1 312 | endfunction 313 | 314 | function! finder#getMatches(items) 315 | let command = printf(b:grepCommand, shellescape(b:query)) 316 | let matchedLines = systemlist(command, b:raw) 317 | let matches = [] 318 | 319 | for line in matchedLines 320 | let separatorIndex = stridx(line, ":") 321 | let key = strpart(line, 0, separatorIndex) - 1 322 | 323 | call add(matches, a:items[key]) 324 | endfor 325 | 326 | return matches 327 | endfunction 328 | 329 | function! finder#comparator(a, b) 330 | return len(a:a.raw) - len(a:b.raw) 331 | endfunction 332 | 333 | function! finder#updateSyntax(pattern) 334 | syntax clear finderMatch 335 | syntax clear finderSelectedMatch 336 | 337 | let pattern = a:pattern 338 | let patternLength = len(pattern) 339 | 340 | let hasCaret = pattern[0] == '^' 341 | let hasDollar = pattern[patternLength - 1] == '$' && (patternLength < 2 || pattern[patternLength - 2] != '\') 342 | 343 | if(hasDollar) 344 | let pattern = strpart(pattern, 0, patternLength - 1) 345 | endif 346 | 347 | if(hasCaret) 348 | let pattern = strpart(pattern, 1) 349 | endif 350 | 351 | let patternLength = len(pattern) 352 | 353 | if(patternLength == 0) 354 | return 355 | endif 356 | 357 | let pattern = escape(pattern, '/') 358 | 359 | if(hasCaret && hasDollar) 360 | let pattern = printf('\zs%s\ze', pattern) 361 | elseif(hasDollar) 362 | let pattern = printf('.*\zs%s\ze', pattern) 363 | elseif(hasCaret) 364 | let pattern = printf('\zs%s\ze.*', pattern) 365 | else 366 | let pattern = printf('\zs%s\ze.*', pattern) 367 | endif 368 | 369 | let matchPattern = printf(b:syntaxMatchRegion, pattern) 370 | let selectedMatchPattern = printf(b:syntaxMatchRegion, pattern) 371 | 372 | silent! execute printf('syntax match finderMatch /%s/ contained', matchPattern) 373 | silent! execute printf('syntax match finderSelectedMatch /%s/ contained', selectedMatchPattern) 374 | endfunction 375 | 376 | function! finder#handler(i) 377 | echo a:i 378 | 379 | return 1 380 | endfunction 381 | 382 | function! finder#getHeader(...) 383 | let options = get(a:, 1, {}) 384 | let include = get(options, "include", [s:HEADER_TITLE, s:HEADER_MATCHES, s:HEADER_MAPPINGS]) 385 | let separator = get(options, "separator", "=") 386 | let separatorWidth = get(options, "separatorWidth", 74) 387 | let header = [] 388 | 389 | call add(header, repeat(separator, separatorWidth)) 390 | 391 | if(index(include, s:HEADER_TITLE) != -1) 392 | call add(header, s:NAME) 393 | endif 394 | 395 | if(index(include, s:HEADER_MATCHES) != -1) 396 | call add(header, ' Matches: ' . s:HEADER_PLACEHOLDER_MATCHES_AMOUNT . '/' . s:HEADER_PLACEHOLDER_ITEMS_AMOUNT) 397 | endif 398 | 399 | if(index(include, s:HEADER_MAPPINGS) != -1) 400 | call add(header, printf(" %s: :start :end :clear :exit :choose", s:HEADER_QUICK_HELP_LABEL)) 401 | endif 402 | 403 | call add(header, repeat(separator, separatorWidth)) 404 | 405 | return header 406 | endfunction 407 | 408 | function! finder#updateHeader() 409 | let header = [] 410 | 411 | for line in b:header 412 | let filledLine = line 413 | 414 | for variable in b:headerVariables 415 | let filledLine = substitute(filledLine, printf(s:HEADER_PLACEHOLDER_PATTERN, variable), getbufvar("%", variable), "g") 416 | endfor 417 | 418 | call add(header, filledLine) 419 | endfor 420 | 421 | call setline(1, header) 422 | endfunction 423 | 424 | function! finder#canGoLeft() 425 | return col('.') > b:queryStartColumn 426 | endfunction 427 | 428 | function! finder#setQueryLine(query) 429 | call setline(b:queryLine, b:prompt . a:query) 430 | call cursor(b:queryLine, col('$')) 431 | endfunction 432 | 433 | function! finder#selectNextItem() 434 | if(b:hoveredLine >= winheight(0) && b:hoveredLine != line('$')) 435 | let b:hoveredLine -= 1 436 | let dict = winsaveview() 437 | 438 | call add(b:hiddenLines, getline(b:firstMatchLine)) 439 | execute b:firstMatchLine . "d" 440 | call winrestview(dict) 441 | endif 442 | 443 | if(b:hoveredLine < line('$')) 444 | call finder#hoverLine(b:hoveredLine + 1) 445 | endif 446 | endfunction 447 | 448 | function! finder#selectPreviousItem() 449 | if(b:hoveredLine == b:firstMatchLine && len(b:hiddenLines) > 0) 450 | let b:hoveredLine += 1 451 | let lastIndex = len(b:hiddenLines) - 1 452 | 453 | call append(b:queryLine, b:hiddenLines[lastIndex]) 454 | call remove(b:hiddenLines, lastIndex) 455 | endif 456 | 457 | if(b:hoveredLine > b:firstMatchLine) 458 | call finder#hoverLine(b:hoveredLine - 1) 459 | endif 460 | endfunction 461 | 462 | function! finder#hoverLine(line) 463 | let b:hoveredLine = a:line 464 | syntax clear finderSelected 465 | execute printf('syntax match finderSelected /\%%%il.*$/ contains=finderSelectedComment,finderSelectedMatch,finderHidden contained', a:line) 466 | 467 | if(b:matchesAmount > 0 && b:keyPressed) 468 | call finder#exciteUserEvent("FinderItemSelected") 469 | endif 470 | endfunction 471 | 472 | function! finder#getSelectedItem() 473 | let key = b:hoveredLine - b:firstMatchLine + len(b:hiddenLines) 474 | 475 | return b:matchesAmount == 0 ? v:null : b:matches[key] 476 | endfunction 477 | 478 | function! finder#get(variable) 479 | return get(s:, a:variable, v:none) 480 | endfunction 481 | 482 | function! finder#error(error) 483 | echo a:error 484 | endfunction 485 | 486 | function! finder#exciteUserEvent(event) 487 | if(exists("#User#" . a:event)) 488 | execute "doautocmd User " . a:event 489 | endif 490 | endfunction 491 | 492 | function! finder#getHeaderVariables(header) 493 | let variables = [] 494 | 495 | for line in a:header 496 | let matches = finder#getStringMatches(line, s:HEADER_PARAMETER_PATTERN, 2) 497 | 498 | for m in matches 499 | call add(variables, m[0]) 500 | endfor 501 | endfor 502 | 503 | return variables 504 | endfunction 505 | 506 | function! finder#getStringMatches(line, pattern, offsetType) 507 | let matches = [] 508 | let offset = 0 509 | 510 | while(1) 511 | let match = matchstrpos(a:line, a:pattern, offset) 512 | 513 | if(match[1] == -1) 514 | return matches 515 | endif 516 | 517 | call add(matches, match) 518 | 519 | let offset = match[a:offsetType] + 1 520 | endwhile 521 | endfunction 522 | 523 | function! finder#getRaw(items) 524 | let b:raw = [] 525 | 526 | for item in a:items 527 | call add(b:raw, item.raw) 528 | endfor 529 | 530 | return b:raw 531 | endfunction 532 | --------------------------------------------------------------------------------