├── .gitignore ├── autoload ├── ripgrep │ ├── event.vim │ ├── observe.vim │ ├── path.vim │ ├── line.vim │ └── job.vim └── ripgrep.vim ├── .github └── workflows │ ├── assign.yml │ └── test.yml ├── test ├── e2e-test.vim ├── observe.vim ├── path.vim └── line.vim ├── LICENSE ├── README.md └── doc └── ripgrep.txt /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /autoload/ripgrep/event.vim: -------------------------------------------------------------------------------- 1 | let g:ripgrep#event#raw = 'raw' 2 | let g:ripgrep#event#error = 'error' 3 | let g:ripgrep#event#other = 'other' 4 | let g:ripgrep#event#match = 'match' 5 | let g:ripgrep#event#file_begin = 'file_begin' 6 | let g:ripgrep#event#file_end = 'file_end' 7 | let g:ripgrep#event#finish = 'finish' 8 | -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Issue assignment 4 | on: 5 | issues: 6 | types: [opened] 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: 'Auto-assign issue' 14 | uses: pozil/auto-assign-issue@v1 15 | with: 16 | assignees: kyoh86 17 | -------------------------------------------------------------------------------- /autoload/ripgrep/observe.vim: -------------------------------------------------------------------------------- 1 | let s:observers = {} 2 | 3 | function! ripgrep#observe#add_observer(name, observer) abort 4 | if !has_key(s:observers, a:name) 5 | let s:observers[a:name] = [] 6 | endif 7 | let l:O = a:observer 8 | if len(filter(copy(s:observers[a:name]), {_, o -> o ==# l:O})) == 0 9 | call add(s:observers[a:name], l:O) 10 | endif 11 | endfunction 12 | 13 | function! ripgrep#observe#notify(name, ...) abort 14 | if !has_key(s:observers, a:name) 15 | return 16 | endif 17 | for l:O in s:observers[a:name] 18 | try 19 | call call(l:O, a:000) 20 | catch 21 | echo v:exception 22 | endtry 23 | endfor 24 | endfunction 25 | -------------------------------------------------------------------------------- /test/e2e-test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('E2E test for ripgrep') 2 | let s:assert = themis#helper('assert') 3 | 4 | function s:suite.test_not_found() 5 | call setqflist([], 'r') 6 | call ripgrep#search("foo" . "bar" . "baz" . "pseudo" . "never" . "found") 7 | call ripgrep#wait(1000) 8 | echo s:assert.length_of(getqflist(), 0, 'quickfix list is empty') 9 | endfunction 10 | 11 | function s:suite.test_found() 12 | call setqflist([], 'r') 13 | call ripgrep#search('foobarbafoobarbazzfoobarbaz test') " TARGET LINE 14 | " TARGET COLUMN -----^ 15 | " TARGET END COLUMN -------^ 16 | call ripgrep#wait(1000) 17 | let l:result = getqflist() 18 | call s:assert.length_of(l:result, 1, 'has 1 item in quickfix list') 19 | let l:first = l:result[0] 20 | call s:assert.equals(l:first['lnum'], 13, "TARGET LINE") 21 | call s:assert.equals(l:first['col'], 26, "TARGET COLUMN") 22 | if has_key(l:first, 'end_col') 23 | call s:assert.equals(l:first['end_col'], 53, "TARGET END COLUMN") 24 | endif 25 | endfunction 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 kyoh86 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 | -------------------------------------------------------------------------------- /test/observe.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('test for ripgrep#line') 2 | let s:assert = themis#helper('assert') 3 | 4 | function! s:suite.add_one_observer() 5 | let l:got = 0 6 | function Increment(local) 7 | let a:local.got += 1 8 | return a:local.got 9 | endfunction 10 | call ripgrep#observe#add_observer("match", function("Increment", [l:])) 11 | call ripgrep#observe#notify("match") 12 | call s:assert.equal(1, l:got) 13 | call ripgrep#observe#notify("match") 14 | call s:assert.equal(2, l:got) 15 | delfunction Increment 16 | endfunction 17 | 18 | function! s:suite.add_two_observer() 19 | let l:got1 = 0 20 | function Increment1(local) 21 | let a:local.got1 += 1 22 | return a:local.got1 23 | endfunction 24 | let l:got2 = 0 25 | function Increment2(local) 26 | let a:local.got2 += 1 27 | return a:local.got2 28 | endfunction 29 | call ripgrep#observe#add_observer("match", function("Increment1", [l:])) 30 | call ripgrep#observe#add_observer("match", function("Increment2", [l:])) 31 | call ripgrep#observe#notify("match") 32 | call s:assert.equal(1, l:got1) 33 | call s:assert.equal(1, l:got2) 34 | call ripgrep#observe#notify("match") 35 | call s:assert.equal(2, l:got1) 36 | call s:assert.equal(2, l:got2) 37 | delfunction Increment1 38 | delfunction Increment2 39 | endfunction 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ripgrep.vim 2 | 3 | [![Test](https://github.com/kyoh86/vim-ripgrep/actions/workflows/test.yml/badge.svg)](https://github.com/kyoh86/vim-ripgrep/actions/workflows/test.yml) 4 | 5 | A plugin for Vim8/Neovim to search text by `ripgrep` (`rg`). 6 | 7 | **THIS IS EXPERIMENTAL PLUGIN AND UNDER DEVELOPMENT.** 8 | **DESTRUCTIVE CHANGES MAY OCCUR.** 9 | 10 | ## What's different from [`jremmen/vim-ripgrep`](https://github.com/jremmen/vim-ripgrep)? 11 | 12 | - Calling `ripgrep` asynchronously. 13 | - Even if it finds a lot of matches, editor won't freeze. 14 | - Exception in case of `Neovim` on `Windows`. 15 | - There's no default command. 16 | - You can create a command with name what you like. 17 | - Observability. 18 | - You can add observer for each event in searching process. 19 | 20 | # USAGE 21 | 22 | For more details: `:help ripgrep.txt` 23 | 24 | ## FUNCTION 25 | 26 | ```vim 27 | :call ripgrep#search('-w --ignore-case foo') 28 | ``` 29 | 30 | ## CONFIG 31 | 32 | You can create a command to call the function with a name you like. 33 | For example: 34 | 35 | ```vim 36 | Plug "kyoh86/vim-ripgrep", 37 | command! -nargs=+ -complete=file Ripgrep :call ripgrep#search() 38 | ``` 39 | 40 | # LICENSE 41 | 42 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](http://www.opensource.org/licenses/MIT) 43 | 44 | This software is released under the [MIT License](http://www.opensource.org/licenses/MIT), see LICENSE. 45 | 46 | - `autoload/ripgrep/job.vim` is from [`async.vim`](https://github.com/prabirshrestha/async.vim) and some patch. 47 | 48 | -------------------------------------------------------------------------------- /autoload/ripgrep/path.vim: -------------------------------------------------------------------------------- 1 | function! ripgrep#path#traverse_root(path, marks) abort 2 | " Search root directory from cwd with root-dir-names 3 | let l:path = s:trim_last_separator(a:path) 4 | if type(a:marks) != v:t_list || len(a:marks) == 0 5 | return [l:path, ''] 6 | endif 7 | let l:found = s:traverse(l:path, '', a:marks) 8 | if l:found is v:null 9 | return [l:path, ''] 10 | endif 11 | return l:found 12 | endfunction 13 | 14 | function! s:traverse(path, rel, marks) abort 15 | if a:path ==# '' 16 | return [a:path, a:rel] 17 | endif 18 | for l:m in a:marks 19 | let l:path = a:path . s:separator() . l:m 20 | if filereadable(l:path) || isdirectory(l:path) 21 | return [a:path, a:rel] 22 | endif 23 | endfor 24 | let l:parent = fnamemodify(a:path, ':h') 25 | if l:parent ==# a:path 26 | return v:null 27 | endif 28 | return s:traverse(l:parent, a:rel . '..' . s:separator(), a:marks) 29 | endfunction 30 | 31 | " Get the directory separator. 32 | let s:sep = v:null 33 | function! s:separator() abort 34 | if s:sep is v:null 35 | let s:sep = fnamemodify('.', ':p')[-1 :] 36 | endif 37 | return s:sep 38 | endfunction 39 | 40 | " Trim end the separator of a:path. 41 | function! s:trim_last_separator(path) abort 42 | let l:sep = s:separator() 43 | let l:pat = escape(l:sep, '\') . '\+$' 44 | return substitute(a:path, l:pat, '', '') 45 | endfunction 46 | 47 | function! ripgrep#path#rel(path) abort 48 | return fnamemodify(a:path, ':~:.') 49 | endfunction 50 | -------------------------------------------------------------------------------- /test/path.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('test for ripgrep#path') 2 | let s:assert = themis#helper('assert') 3 | 4 | let s:tempname = tempname() 5 | 6 | function s:suite.before() 7 | call mkdir(s:tempname, 'p') 8 | call mkdir(s:tempname . '/current/mark', 'p') 9 | call mkdir(s:tempname . '/parent/child', 'p') 10 | call mkdir(s:tempname . '/parent/mark', 'p') 11 | call mkdir(s:tempname . '/ancestor/parent/child', 'p') 12 | call mkdir(s:tempname . '/ancestor/mark', 'p') 13 | endfunction 14 | 15 | function s:suite.after() 16 | call delete(s:tempname, 'rf') 17 | endfunction 18 | 19 | function s:suite.test_traverse_current() 20 | let l:from = s:tempname . '/current' 21 | let l:want = [l:from, ''] 22 | let l:got = ripgrep#path#traverse_root(l:from, ['mark']) 23 | call s:assert.equals(l:got, l:want) 24 | endfunction 25 | 26 | function s:suite.test_traverse_parent() 27 | let l:from = s:tempname . '/parent/child' 28 | let l:want_0 = s:tempname . '/parent' 29 | let l:want_1_pattern = '\.\.[/\\]' 30 | let l:got = ripgrep#path#traverse_root(l:from, ['mark']) 31 | call s:assert.is_list(l:got) 32 | call s:assert.length_of(l:got, 2) 33 | call s:assert.equals(l:want_0, l:got[0]) 34 | call s:assert.match(l:got[1], l:want_1_pattern) 35 | endfunction 36 | 37 | function s:suite.test_traverse_second_mark() 38 | let l:from = s:tempname . '/ancestor/parent/child' 39 | let l:want_0 = s:tempname . '/ancestor' 40 | let l:want_1_pattern = '\.\.[/\\]\.\.[/\\]' 41 | let l:got = ripgrep#path#traverse_root(l:from, ['pseudo-mark', 'mark']) 42 | call s:assert.is_list(l:got) 43 | call s:assert.length_of(l:got, 2) 44 | call s:assert.equals(l:want_0, l:got[0]) 45 | call s:assert.match(l:got[1], l:want_1_pattern) 46 | endfunction 47 | 48 | function s:suite.test_traverse_not_found() 49 | let l:from = s:tempname . '/ancestor/parent/child' 50 | let l:want = [l:from, ''] 51 | let l:got = ripgrep#path#traverse_root(l:from, ['pseudo-mark']) 52 | call s:assert.equals(l:got, l:want) 53 | endfunction 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Unit tests on Ubuntu 7 | timeout-minutes: 20 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [macos-latest, ubuntu-latest] 12 | neovim: [true, false] 13 | version: [stable, nightly] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup cargo 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | - name: install ripgrep 22 | run: cargo install ripgrep 23 | - name: Checkout vim-themis 24 | uses: actions/checkout@v4 25 | with: 26 | repository: thinca/vim-themis 27 | path: vim-themis 28 | - uses: rhysd/action-setup-vim@v1 29 | id: vim 30 | with: 31 | version: ${{ matrix.version }} 32 | neovim: ${{ matrix.neovim }} 33 | - name: Run unit tests 34 | env: 35 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 36 | run: ./vim-themis/bin/themis ./test 37 | 38 | test-windows: 39 | name: Unit tests on Windows 40 | timeout-minutes: 10 41 | strategy: 42 | matrix: 43 | neovim: [true, false] 44 | version: [stable, nightly] 45 | exclude: 46 | # No stable Vim release is officially provided for Windows 47 | - neovim: false 48 | version: stable 49 | runs-on: windows-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Setup cargo 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: stable 56 | - name: install ripgrep 57 | run: cargo install ripgrep 58 | - name: Checkout vim-themis 59 | uses: actions/checkout@v4 60 | with: 61 | repository: thinca/vim-themis 62 | path: vim-themis 63 | - uses: rhysd/action-setup-vim@v1 64 | id: vim 65 | with: 66 | version: ${{ matrix.version }} 67 | neovim: ${{ matrix.neovim }} 68 | - name: Run unit tests 69 | env: 70 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 71 | run: ./vim-themis/bin/themis ./test 72 | -------------------------------------------------------------------------------- /test/line.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('test for ripgrep#line') 2 | let s:assert = themis#helper('assert') 3 | 4 | function! s:suite.parse_empty() 5 | let l:from = '' 6 | let l:want = [g:ripgrep#event#raw, {'raw': ''}] 7 | let l:got = ripgrep#line#parse(l:from) 8 | call s:assert.equals(l:want, l:got) 9 | endfunction 10 | 11 | function! s:suite.bared_value() 12 | let l:from = '0' 13 | let l:want = [g:ripgrep#event#raw, {'raw': '0'}] 14 | let l:got = ripgrep#line#parse(l:from) 15 | call s:assert.equals(l:want, l:got) 16 | endfunction 17 | 18 | function! s:suite.begin_line() 19 | let l:from = '{"type":"begin","data":{"path":{"text":"test-filename"}}}' 20 | let l:want = [g:ripgrep#event#file_begin, {'filename': 'test-filename'}] 21 | let l:got = ripgrep#line#parse(l:from) 22 | call s:assert.equals(l:want, l:got) 23 | endfunction 24 | 25 | function! s:suite.end_line() 26 | let l:from = '{"type":"end","data":{"path":{"text":"test-filename"},"stats":"stats-value"}}' 27 | let l:want = [g:ripgrep#event#file_end, {'filename': 'test-filename', 'stats': 'stats-value'}] 28 | let l:got = ripgrep#line#parse(l:from) 29 | call s:assert.equals(l:want, l:got) 30 | endfunction 31 | 32 | function! s:suite.match_line() 33 | let l:from = '{"type":"match","data":{"path":{"text":"test-filename"},"lines":{"text":"test-lines"},"line_number":2,"submatches":[{"start":3,"end":4}]}}' 34 | let l:want = [g:ripgrep#event#match, {'filename': 'test-filename', 'text': 'test-lines', 'lnum': 2, 'col': 4, 'end_col': 5}] 35 | let l:got = ripgrep#line#parse(l:from) 36 | call s:assert.equals(l:want, l:got) 37 | endfunction 38 | 39 | function! s:suite.match_binary() 40 | " base64: 'Zml6eg==' => text: 'fizz' 41 | let l:from = '{"type":"match","data":{"path":{"text":"test-filename"},"lines":{"bytes":"Zml6eg=="},"line_number":2,"submatches":[{"start":3,"end":4}]}}' 42 | let l:want = [g:ripgrep#event#match, {'filename': 'test-filename', 'text': 'fizz', 'lnum': 2, 'col': 4, 'end_col': 5}] 43 | let l:got = ripgrep#line#parse(l:from) 44 | call s:assert.equals(l:want, l:got) 45 | endfunction 46 | 47 | function! s:suite.other_line() 48 | let l:from = '{"foo":"bar","bar":17}' 49 | let l:want = [g:ripgrep#event#other, {'foo': 'bar', 'bar': 17}] 50 | let l:got = ripgrep#line#parse(l:from) 51 | call s:assert.equals(l:want, l:got) 52 | endfunction 53 | 54 | function! s:suite.decode_base64_foo() 55 | let l:from = 'Zm9v' 56 | let l:want = 'foo' 57 | let l:got = ripgrep#line#decode_base64(l:from) 58 | call s:assert.equals(l:got, l:want) 59 | endfunction 60 | 61 | function! s:suite.decode_base64_fizz() 62 | let l:from = 'Zml6eg==' 63 | let l:want = 'fizz' 64 | let l:got = ripgrep#line#decode_base64(l:from) 65 | call s:assert.equals(l:got, l:want) 66 | endfunction 67 | -------------------------------------------------------------------------------- /autoload/ripgrep/line.vim: -------------------------------------------------------------------------------- 1 | " line.vim 2 | " 3 | " Functions to process output lines from rg --json 4 | 5 | let s:remain = '' 6 | 7 | function! ripgrep#line#parse(line) abort 8 | let l:line = s:remain .. a:line 9 | 10 | " Parse json-line from ripgrep (with --json option) to qf-list item. 11 | let l:line_object = v:null 12 | try 13 | let l:line_object = json_decode(a:line) 14 | catch 15 | endtry 16 | 17 | if type(l:line_object) != v:t_dict 18 | let s:remain = l:line 19 | return [g:ripgrep#event#raw, {'raw': a:line}] 20 | else 21 | let s:remain = '' 22 | endif 23 | 24 | let l:type = get(l:line_object, 'type', '') 25 | if l:type ==# 'begin' 26 | return s:process_begin(l:line_object) 27 | elseif l:type ==# 'match' 28 | return s:process_match(l:line_object) 29 | elseif l:type ==# 'end' 30 | return s:process_end(l:line_object) 31 | else 32 | return [g:ripgrep#event#other, l:line_object] 33 | endif 34 | endfunction 35 | 36 | function! s:process_begin(line_object) abort 37 | " Parse beginning of file from ripgrep. 38 | let l:begin = a:line_object['data'] 39 | let l:filename = s:process_textlike(l:begin['path']) 40 | if l:filename is v:null 41 | return [g:ripgrep#event#other, a:line_object] 42 | endif 43 | return [g:ripgrep#event#file_begin, {'filename': l:filename}] 44 | endfunction 45 | 46 | function! s:process_end(line_object) abort 47 | " Parse ending of file from ripgrep. 48 | let l:end = a:line_object['data'] 49 | let l:filename = s:process_textlike(l:end['path']) 50 | if l:filename is v:null 51 | return [g:ripgrep#event#other, a:line_object] 52 | endif 53 | return [g:ripgrep#event#file_end, {'filename': l:filename, 'stats': l:end['stats']}] 54 | endfunction 55 | 56 | function! s:process_match(line_object) abort 57 | " Parse match-data from ripgrep to qf-list item. 58 | let l:match = a:line_object['data'] 59 | let l:filename = s:process_textlike(l:match['path']) 60 | if l:filename is v:null 61 | return [g:ripgrep#event#other, a:line_object] 62 | endif 63 | let l:lnum = l:match['line_number'] 64 | let l:submatches = l:match['submatches'] 65 | " The start is based 0. 66 | let l:start = l:submatches[0]['start'] + 1 67 | let l:end = l:submatches[0]['end'] + 1 68 | 69 | 70 | let l:linetext = s:process_textlike(l:match['lines']) 71 | if l:linetext is v:null 72 | return [g:ripgrep#event#other, a:line_object] 73 | end 74 | let l:linetext = trim(l:linetext, "\n", 2) 75 | return [ 76 | \ g:ripgrep#event#match, { 77 | \ 'filename': l:filename, 78 | \ 'lnum': l:lnum, 79 | \ 'col': l:start, 80 | \ 'end_col': l:end, 81 | \ 'text': l:linetext, 82 | \ } 83 | \ ] 84 | endfunction 85 | 86 | function! s:process_textlike(textlike) abort 87 | " Decode an object like a text: 88 | " - (text): {'text':'value'} (raw string) 89 | " - (text): {'bytes':"Zml6eg=="} (base 64 encoded) 90 | if has_key(a:textlike, 'text') 91 | return a:textlike['text'] 92 | elseif has_key(a:textlike, 'bytes') 93 | return ripgrep#line#decode_base64(a:textlike['bytes']) 94 | else 95 | return v:null 96 | end 97 | endfunction 98 | 99 | let s:base64_table = [ 100 | \ 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', 101 | \ 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 102 | \ '0','1','2','3','4','5','6','7','8','9','+','/'] 103 | let s:base64_pad = '=' 104 | 105 | let s:base64_atoi_table = {} 106 | function! s:decode_base64_atoi(a) abort 107 | if len(s:base64_atoi_table) == 0 108 | for l:i in range(len(s:base64_table)) 109 | let s:base64_atoi_table[s:base64_table[l:i]] = l:i 110 | endfor 111 | endif 112 | return s:base64_atoi_table[a:a] 113 | endfunction 114 | 115 | function! ripgrep#line#decode_base64(seq) abort 116 | let l:res = '' 117 | for l:i in range(0, len(a:seq) - 1, 4) 118 | let l:n = s:decode_base64_atoi(a:seq[l:i]) * 0x40000 119 | \ + s:decode_base64_atoi(a:seq[l:i + 1]) * 0x1000 120 | \ + (a:seq[l:i + 2] == s:base64_pad ? 0 : s:decode_base64_atoi(a:seq[l:i + 2])) * 0x40 121 | \ + (a:seq[l:i + 3] == s:base64_pad ? 0 : s:decode_base64_atoi(a:seq[l:i + 3])) 122 | let l:res = s:add_str(l:res, l:n / 0x10000) 123 | let l:res = s:add_str(l:res, l:n / 0x100 % 0x100) 124 | let l:res = s:add_str(l:res, l:n % 0x100) 125 | endfor 126 | return eval('"' . l:res . '"') 127 | endfunction 128 | 129 | function! s:add_str(left, right) abort 130 | if a:right == 0 131 | return a:left 132 | endif 133 | return a:left . printf("\\x%02x", a:right) 134 | endfunction 135 | -------------------------------------------------------------------------------- /autoload/ripgrep.vim: -------------------------------------------------------------------------------- 1 | function! s:get_executable() abort 2 | " Get rip-grep executable from global variable 3 | if exists('g:ripgrep#executable') 4 | return g:ripgrep#executable 5 | endif 6 | if has('win32') 7 | return 'rg.exe' 8 | else 9 | return 'rg' 10 | endif 11 | endfunction 12 | 13 | function! s:get_root_marks() abort 14 | " Get rip-grep root directory marks from global variable. 15 | " Default: [".git"] 16 | if exists('g:ripgrep#root_marks') 17 | return g:ripgrep#root_marks 18 | endif 19 | return ['.git'] 20 | endfunction 21 | 22 | function! s:get_base_options() abort 23 | " Get common command-line options for ripgrep. 24 | " It uses 'ignorecase' and 'smartcase' vim option. 25 | let l:opts = ['--json', '--no-line-buffered', '--no-block-buffered'] 26 | if &ignorecase == 1 27 | call add(l:opts, '--ignore-case') 28 | endif 29 | if &smartcase == 1 30 | call add(l:opts, '--smart-case') 31 | endif 32 | return l:opts 33 | endfunction 34 | 35 | let s:found = v:false 36 | let s:jobid = 0 37 | 38 | function! s:reset() abort 39 | " Reset (initialize) job status and quickfix-list 40 | let s:found = v:false 41 | call ripgrep#stop() 42 | call setqflist([], ' ') 43 | call ripgrep#observe#add_observer(g:ripgrep#event#match, 'ripgrep#__register_match') 44 | endfunction 45 | 46 | function! ripgrep#__register_match(item) abort 47 | if !s:found 48 | copen 49 | endif 50 | let s:found = v:true 51 | call setqflist([a:item], 'a') 52 | endfunction 53 | 54 | function! s:finish(arg, status) abort 55 | " Finish quickfix-list 56 | if s:found 57 | let l:title = 'Ripgrep' 58 | if a:arg !=# '' 59 | let l:title = l:title . ' ' . a:arg 60 | endif 61 | call setqflist([], 'a', {'title': l:title}) 62 | let s:jobid = 0 63 | end 64 | call ripgrep#observe#notify(g:ripgrep#event#finish, {'status': a:status}) 65 | endfunction 66 | 67 | function! s:stdout_handler_core(rel, job_id, data, event_type) abort 68 | " Receive lines from rg --json 69 | for l:line in a:data 70 | let l:handler = ripgrep#line#parse(l:line) 71 | let l:event = l:handler[0] 72 | let l:body = l:handler[1] 73 | if has_key(l:body, 'filename') 74 | let l:body['filename'] = a:rel . l:body['filename'] 75 | endif 76 | call ripgrep#observe#notify(l:event, l:body) 77 | endfor 78 | endfunction 79 | 80 | function! s:get_stdout_handler(rel) abort 81 | return {job_id, data, event_type -> s:stdout_handler_core(a:rel, job_id, data, event_type)} 82 | endfunction 83 | 84 | function! s:stderr_handler(job_id, data, event_type) abort 85 | " Receive standard-error lines from rg --json 86 | for l:line in a:data 87 | call ripgrep#observe#notify(g:ripgrep#event#error, {'raw': l:line}) 88 | endfor 89 | endfunction 90 | 91 | function! s:exit_handler(arg, job_id, data, event_type) abort 92 | let l:status = a:data 93 | call s:finish(a:arg, l:status) 94 | if l:status != 0 95 | echomsg 'failed to find' 96 | endif 97 | endfunction 98 | 99 | function! s:call(cmd, arg, cwd, rel) abort 100 | let l:cmd = a:cmd 101 | if a:arg !=# '' 102 | let l:cmd = l:cmd . ' ' . a:arg 103 | endif 104 | call s:reset() 105 | 106 | let s:jobid = ripgrep#job#start(l:cmd, { 107 | \ 'on_stdout': s:get_stdout_handler(a:rel), 108 | \ 'on_stderr': function('s:stderr_handler'), 109 | \ 'on_exit': function('s:exit_handler', [a:arg]), 110 | \ 'normalize': 'array', 111 | \ 'overlapped': v:true, 112 | \ 'cwd': a:cwd, 113 | \ }) 114 | if s:jobid <= 0 115 | echoerr 'failed to be call ripgrep' 116 | endif 117 | endfunction 118 | 119 | function! ripgrep#search(arg) abort 120 | " get cwd (tuple of [path, rel]) 121 | let l:cwd = ripgrep#path#traverse_root(getcwd(), s:get_root_marks()) 122 | 123 | " get base command 124 | let l:exe = s:get_executable() 125 | if !executable(l:exe) 126 | echoerr "There's no executable: " . l:exe 127 | endif 128 | let l:cmds = [l:exe] 129 | call extend(l:cmds, s:get_base_options()) 130 | 131 | let l:cmd = join(l:cmds, ' ') 132 | call s:call(l:cmd, a:arg, l:cwd[0], l:cwd[1]) 133 | endfunction 134 | 135 | function! ripgrep#call(cmd, cwd, rel) abort 136 | call s:call(a:cmd, '', a:cwd, a:rel) 137 | endfunction 138 | 139 | function! ripgrep#stop() abort 140 | if s:jobid <= 0 141 | return 142 | endif 143 | silent call ripgrep#job#stop(s:jobid) 144 | let s:jobid = 0 145 | endfunction 146 | 147 | function! ripgrep#wait(...) abort 148 | " ripgrep#wait([{timeout}]) wait current process 149 | if s:jobid <= 0 150 | return 151 | endif 152 | try 153 | let l:timeout = get(a:000, 0, -1) 154 | call ripgrep#job#wait([s:jobid], l:timeout) 155 | catch 156 | endtry 157 | endfunction 158 | -------------------------------------------------------------------------------- /doc/ripgrep.txt: -------------------------------------------------------------------------------- 1 | *ripgrep.txt* A plugin to search text by ripgrep 2 | 3 | THIS IS EXPERIMENTAL PLUGIN AND UNDER DEVELOPMENT. 4 | DESTRUCTIVE CHANGES MAY OCCUR. 5 | 6 | Author: kyoh86 7 | License: MIT license 8 | 9 | =============================================================================== 10 | CONTENTS *ripgrep-contents* 11 | 12 | USAGE |ripgrep-usage| 13 | FUNCTIONS |ripgrep-function| 14 | COMMANDS |ripgrep-command| 15 | VARIABLES |ripgrep-variable| 16 | 17 | =============================================================================== 18 | USAGE *ripgrep-usage* 19 | 20 | *ripgrep* provides functions to search text by ripgrep(`rg`), to watch result, 21 | and set results in quickfix. 22 | 23 | ------------------------------------------------------------------------------- 24 | FUNCTIONS *ripgrep-function* 25 | 26 | *ripgrep#search()* 27 | ripgrep#search({arg}) 28 | 29 | Search keyword from current working directory by ripgrep (`rg`) command. 30 | {arg} is |String| may be pass for the ripgrep like below: 31 | > 32 | call ripgrep#search('-w --ignore-case foo') 33 | < 34 | See `$ rg --help` for more details. 35 | 36 | *ripgrep#call()* 37 | ripgrep#call({cmd}, {cwd}, {rel}) 38 | 39 | Call ripgrep with the specified cmd, cwd. 40 | - {cmd} is command-line |String| to call the ripgrep. 41 | - {cwd} is a path |String| of the working-directory to execute {cmd}. 42 | - {rel} is prefix |String| for filenames in the results. 43 | 44 | Example: > 45 | cd /tmp/foo/bar/baz 46 | call ripgrep#call('rg --json -w --ignore-case foo', '/tmp/foo', '../../') 47 | < 48 | Warning: You must pass a command-line calling ripgrep with the `--json` 49 | option. 50 | 51 | *ripgrep#stop()* 52 | ripgrep#stop() 53 | 54 | Stop `rg` in background process. 55 | 56 | 57 | *ripgrep#wait()* 58 | ripgrep#wait([{timeout}]) 59 | 60 | Wait `rg` in background process being finished. 61 | It will be timeouted after {timeout} milli-second. 62 | 63 | 64 | *ripgrep#observe#add_observer()* 65 | ripgrep#observe#add_observer({name}, {func}) 66 | Add an observer to receive some events. 67 | All events send a message to {func} with a |Dictionary| parameter. 68 | 69 | Events: 70 | - `g:ripgrep#event#file_begin` 71 | Indicate that ripgrep begins searching in a file. 72 | Parameter: 73 | - `filename`: a path |String| searching. 74 | 75 | - `g:ripgrep#event#match` 76 | 77 | Indicate that ripgrep find a line matching the keyword. 78 | Parameter: 79 | - `filename`: a path |String| matched. 80 | - `lnum`: a line |Number| matched. 81 | - `col`: a column |Number| matched. 82 | - `end_col`: an end-column |Number| matched. 83 | - `text`: a content |String| matched. 84 | 85 | - `g:ripgrep#event#file_end` 86 | Indicate that ripgrep ended searching in a file. 87 | Parameter: 88 | - `filename`: a path |String| searching. 89 | - `stats`: stats |Dictionary| from ripgrep. 90 | 91 | - `g:ripgrep#event#finish` 92 | Indicate that ripgrep stopped. 93 | Parameter: 94 | - `status`: a status code |Number|. 95 | 96 | - `g:ripgrep#event#other` 97 | Indicate that an other json-entry is reported from ripgrep. 98 | Parameter is a |Dictionary| parsed from the json-line. 99 | 100 | - `g:ripgrep#event#error` 101 | Indicate that ripgrep puts a line into standard-error. 102 | Parameter: 103 | - `raw`: a raw line |String|. 104 | 105 | - `g:ripgrep#event#raw` 106 | Indicate that ripgrep puts a line being not formed as json. 107 | Parameter: 108 | - `raw`: a raw line |String|. 109 | 110 | Usage examples: 111 | > 112 | function! ReceiveRipgrepBegin(params) 113 | echo a:params['filename'] 114 | endfunction 115 | call ripgrep#observe#add_observer( 116 | \ ripgrep#event#file_begin, 117 | \ "ReceiveRipgrepBegin" 118 | \ ) 119 | < 120 | 121 | > 122 | function! ReceiveRipgrepMatch(params) 123 | echo a:params['filename'] 124 | echo a:params['lnum'] 125 | echo a:params['col'] 126 | echo a:params['end_col'] 127 | echo a:params['text'] 128 | endfunction 129 | call ripgrep#observe#add_observer( 130 | \ ripgrep#event#match, 131 | \ "ReceiveRipgrepMatch" 132 | \ ) 133 | < 134 | 135 | 136 | ------------------------------------------------------------------------------- 137 | COMMANDS *ripgrep-command* 138 | 139 | THIS PLUGIN NEVER DEFINES ANY COMMANDS. 140 | If you want call any functions from command, you can define them like below. 141 | 142 | *:Ripgrep* 143 | :Ripgrep {keyword} [{opts}...] 144 | 145 | Setting: 146 | > 147 | command! -nargs=+ -complete=file Ripgrep :call ripgrep#search() 148 | < 149 | 150 | Search keyword from current working directory by ripgrep (`rg`) command. 151 | {opts} also may be pass for the ripgrep like below: 152 | > 153 | :Ripgrep -w --ignore-case foo 154 | < 155 | See `$ rg --help` for more details. 156 | 157 | 158 | ------------------------------------------------------------------------------- 159 | VARIABLE *ripgrep-variable* 160 | 161 | *g:ripgrep#executable* 162 | Set a path |String| to executable "ripgrep" command. 163 | Default: v:null 164 | 165 | *g:ripgrep#root_marks* 166 | Set mark |List| to find root-directory. 167 | Default: [".git"] 168 | 169 | vim:filetype=help 170 | -------------------------------------------------------------------------------- /autoload/ripgrep/job.vim: -------------------------------------------------------------------------------- 1 | " https://github.com/prabirshrestha/async.vim#2082d13bb195f3203d41a308b89417426a7deca1 (dirty) 2 | " :AsyncEmbed path=./autoload/ripgrep/job.vim namespace=ripgrep#job 3 | 4 | " Author: Prabir Shrestha 5 | " Website: https://github.com/prabirshrestha/async.vim 6 | " License: The MIT License {{{ 7 | " The MIT License (MIT) 8 | " 9 | " Copyright (c) 2016 Prabir Shrestha 10 | " 11 | " Permission is hereby granted, free of charge, to any person obtaining a copy 12 | " of this software and associated documentation files (the "Software"), to deal 13 | " in the Software without restriction, including without limitation the rights 14 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | " copies of the Software, and to permit persons to whom the Software is 16 | " furnished to do so, subject to the following conditions: 17 | " 18 | " The above copyright notice and this permission notice shall be included in all 19 | " copies or substantial portions of the Software. 20 | " 21 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | " SOFTWARE. 28 | " }}} 29 | 30 | let s:save_cpo = &cpo 31 | set cpo&vim 32 | 33 | let s:jobidseq = 0 34 | let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'} 35 | let s:job_type_nvimjob = 'nvimjob' 36 | let s:job_type_vimjob = 'vimjob' 37 | let s:job_error_unsupported_job_type = -2 " unsupported job type 38 | 39 | function! s:noop(...) abort 40 | endfunction 41 | 42 | function! s:job_supported_types() abort 43 | let l:supported_types = [] 44 | if has('nvim') 45 | let l:supported_types += [s:job_type_nvimjob] 46 | endif 47 | if !has('nvim') && has('job') && has('channel') && has('lambda') 48 | let l:supported_types += [s:job_type_vimjob] 49 | endif 50 | return l:supported_types 51 | endfunction 52 | 53 | function! s:job_supports_type(type) abort 54 | return index(s:job_supported_types(), a:type) >= 0 55 | endfunction 56 | 57 | function! s:out_cb(jobid, opts, job, data) abort 58 | call a:opts.on_stdout(a:jobid, a:data, 'stdout') 59 | endfunction 60 | 61 | function! s:out_cb_array(jobid, opts, job, data) abort 62 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 63 | endfunction 64 | 65 | function! s:err_cb(jobid, opts, job, data) abort 66 | call a:opts.on_stderr(a:jobid, a:data, 'stderr') 67 | endfunction 68 | 69 | function! s:err_cb_array(jobid, opts, job, data) abort 70 | call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr') 71 | endfunction 72 | 73 | function! s:exit_cb(jobid, opts, job, status) abort 74 | if has_key(a:opts, 'on_exit') 75 | call a:opts.on_exit(a:jobid, a:status, 'exit') 76 | endif 77 | if has_key(s:jobs, a:jobid) 78 | call remove(s:jobs, a:jobid) 79 | endif 80 | endfunction 81 | 82 | function! s:on_stdout(jobid, data, event) abort 83 | let l:jobinfo = s:jobs[a:jobid] 84 | call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event) 85 | endfunction 86 | 87 | function! s:on_stdout_string(jobid, data, event) abort 88 | let l:jobinfo = s:jobs[a:jobid] 89 | call l:jobinfo.opts.on_stdout(a:jobid, join(a:data, "\n"), a:event) 90 | endfunction 91 | 92 | function! s:on_stderr(jobid, data, event) abort 93 | let l:jobinfo = s:jobs[a:jobid] 94 | call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event) 95 | endfunction 96 | 97 | function! s:on_stderr_string(jobid, data, event) abort 98 | let l:jobinfo = s:jobs[a:jobid] 99 | call l:jobinfo.opts.on_stderr(a:jobid, join(a:data, "\n"), a:event) 100 | endfunction 101 | 102 | function! s:on_exit(jobid, status, event) abort 103 | if has_key(s:jobs, a:jobid) 104 | let l:jobinfo = s:jobs[a:jobid] 105 | if has_key(l:jobinfo.opts, 'on_exit') 106 | call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event) 107 | endif 108 | if has_key(s:jobs, a:jobid) 109 | call remove(s:jobs, a:jobid) 110 | endif 111 | endif 112 | endfunction 113 | 114 | function! s:job_start(cmd, opts) abort 115 | let l:jobtypes = s:job_supported_types() 116 | let l:jobtype = '' 117 | 118 | if has_key(a:opts, 'type') 119 | if type(a:opts.type) == type('') 120 | if !s:job_supports_type(a:opts.type) 121 | return s:job_error_unsupported_job_type 122 | endif 123 | let l:jobtype = a:opts.type 124 | else 125 | let l:jobtypes = a:opts.type 126 | endif 127 | endif 128 | 129 | if empty(l:jobtype) 130 | " find the best jobtype 131 | for l:jobtype2 in l:jobtypes 132 | if s:job_supports_type(l:jobtype2) 133 | let l:jobtype = l:jobtype2 134 | endif 135 | endfor 136 | endif 137 | 138 | if l:jobtype ==? '' 139 | return s:job_error_unsupported_job_type 140 | endif 141 | 142 | " options shared by both vim and neovim 143 | let l:jobopt = {} 144 | if has_key(a:opts, 'cwd') 145 | let l:jobopt.cwd = a:opts.cwd 146 | endif 147 | if has_key(a:opts, 'env') 148 | let l:jobopt.env = a:opts.env 149 | endif 150 | 151 | let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw 152 | 153 | if l:jobtype == s:job_type_nvimjob 154 | if l:normalize ==# 'string' 155 | let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout_string') : function('s:noop') 156 | let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr_string') : function('s:noop') 157 | else " array or raw 158 | let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout') : function('s:noop') 159 | let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr') : function('s:noop') 160 | endif 161 | let l:jobopt['stdin'] = 'null' 162 | call extend(l:jobopt, { 'on_exit': function('s:on_exit') }) 163 | let l:job = jobstart(a:cmd, l:jobopt) 164 | if l:job <= 0 165 | return l:job 166 | endif 167 | let l:jobid = l:job " nvimjobid and internal jobid is same 168 | let s:jobs[l:jobid] = { 169 | \ 'type': s:job_type_nvimjob, 170 | \ 'opts': a:opts, 171 | \ } 172 | let s:jobs[l:jobid].job = l:job 173 | elseif l:jobtype == s:job_type_vimjob 174 | let s:jobidseq = s:jobidseq + 1 175 | let l:jobid = s:jobidseq 176 | if l:normalize ==# 'array' 177 | let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb_array', [l:jobid, a:opts]) : function('s:noop') 178 | let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb_array', [l:jobid, a:opts]) : function('s:noop') 179 | else " raw or string 180 | let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb', [l:jobid, a:opts]) : function('s:noop') 181 | let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb', [l:jobid, a:opts]) : function('s:noop') 182 | endif 183 | let l:jobopt['in_io'] = 'null' 184 | call extend(l:jobopt, { 185 | \ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]), 186 | \ 'mode': 'raw', 187 | \ }) 188 | if has('patch-8.1.889') 189 | let l:jobopt['noblock'] = 1 190 | endif 191 | let l:job = job_start(a:cmd, l:jobopt) 192 | if job_status(l:job) !=? 'run' 193 | return -1 194 | endif 195 | let s:jobs[l:jobid] = { 196 | \ 'type': s:job_type_vimjob, 197 | \ 'opts': a:opts, 198 | \ 'job': l:job, 199 | \ 'channel': job_getchannel(l:job), 200 | \ 'buffer': '' 201 | \ } 202 | else 203 | return s:job_error_unsupported_job_type 204 | endif 205 | 206 | return l:jobid 207 | endfunction 208 | 209 | function! s:job_stop(jobid) abort 210 | if has_key(s:jobs, a:jobid) 211 | let l:jobinfo = s:jobs[a:jobid] 212 | if l:jobinfo.type == s:job_type_nvimjob 213 | " See: vital-Whisky/System.Job 214 | try 215 | call jobstop(a:jobid) 216 | catch /^Vim\%((\a\+)\)\=:E900/ 217 | " NOTE: 218 | " Vim does not raise exception even the job has already closed so fail 219 | " silently for 'E900: Invalid job id' exception 220 | endtry 221 | elseif l:jobinfo.type == s:job_type_vimjob 222 | if type(s:jobs[a:jobid].job) == v:t_job 223 | call job_stop(s:jobs[a:jobid].job) 224 | elseif type(s:jobs[a:jobid].job) == v:t_channel 225 | call ch_close(s:jobs[a:jobid].job) 226 | endif 227 | endif 228 | endif 229 | endfunction 230 | 231 | function! s:job_send(jobid, data, opts) abort 232 | let l:jobinfo = s:jobs[a:jobid] 233 | let l:close_stdin = get(a:opts, 'close_stdin', 0) 234 | if l:jobinfo.type == s:job_type_nvimjob 235 | call jobsend(a:jobid, a:data) 236 | if l:close_stdin 237 | call chanclose(a:jobid, 'stdin') 238 | endif 239 | elseif l:jobinfo.type == s:job_type_vimjob 240 | " There is no easy way to know when ch_sendraw() finishes writing data 241 | " on a non-blocking channels -- has('patch-8.1.889') -- and because of 242 | " this, we cannot safely call ch_close_in(). So when we find ourselves 243 | " in this situation (i.e. noblock=1 and close stdin after send) we fall 244 | " back to using s:flush_vim_sendraw() and wait for transmit buffer to be 245 | " empty 246 | " 247 | " Ref: https://groups.google.com/d/topic/vim_dev/UNNulkqb60k/discussion 248 | if has('patch-8.1.818') && (!has('patch-8.1.889') || !l:close_stdin) 249 | call ch_sendraw(l:jobinfo.channel, a:data) 250 | else 251 | let l:jobinfo.buffer .= a:data 252 | call s:flush_vim_sendraw(a:jobid, v:null) 253 | endif 254 | if l:close_stdin 255 | while len(l:jobinfo.buffer) != 0 256 | sleep 1m 257 | endwhile 258 | call ch_close_in(l:jobinfo.channel) 259 | endif 260 | endif 261 | endfunction 262 | 263 | function! s:flush_vim_sendraw(jobid, timer) abort 264 | " https://github.com/vim/vim/issues/2548 265 | " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 266 | let l:jobinfo = s:jobs[a:jobid] 267 | sleep 1m 268 | if len(l:jobinfo.buffer) <= 4096 269 | call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer) 270 | let l:jobinfo.buffer = '' 271 | else 272 | let l:to_send = l:jobinfo.buffer[:4095] 273 | let l:jobinfo.buffer = l:jobinfo.buffer[4096:] 274 | call ch_sendraw(l:jobinfo.channel, l:to_send) 275 | call timer_start(1, function('s:flush_vim_sendraw', [a:jobid])) 276 | endif 277 | endfunction 278 | 279 | function! s:job_wait_single(jobid, timeout, start) abort 280 | if !has_key(s:jobs, a:jobid) 281 | return -3 282 | endif 283 | 284 | let l:jobinfo = s:jobs[a:jobid] 285 | if l:jobinfo.type == s:job_type_nvimjob 286 | let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000 287 | return jobwait([a:jobid], float2nr(l:timeout))[0] 288 | elseif l:jobinfo.type == s:job_type_vimjob 289 | let l:timeout = a:timeout / 1000.0 290 | try 291 | while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout 292 | let l:info = job_info(l:jobinfo.job) 293 | if l:info.status ==# 'dead' 294 | return l:info.exitval 295 | elseif l:info.status ==# 'fail' 296 | return -3 297 | endif 298 | sleep 1m 299 | endwhile 300 | catch /^Vim:Interrupt$/ 301 | return -2 302 | endtry 303 | endif 304 | return -1 305 | endfunction 306 | 307 | function! s:job_wait(jobids, timeout) abort 308 | let l:start = reltime() 309 | let l:exitcode = 0 310 | let l:ret = [] 311 | for l:jobid in a:jobids 312 | if l:exitcode != -2 " Not interrupted. 313 | let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start) 314 | endif 315 | let l:ret += [l:exitcode] 316 | endfor 317 | return l:ret 318 | endfunction 319 | 320 | function! s:job_pid(jobid) abort 321 | if !has_key(s:jobs, a:jobid) 322 | return 0 323 | endif 324 | 325 | let l:jobinfo = s:jobs[a:jobid] 326 | if l:jobinfo.type == s:job_type_nvimjob 327 | return jobpid(a:jobid) 328 | elseif l:jobinfo.type == s:job_type_vimjob 329 | let l:vimjobinfo = job_info(a:jobid) 330 | if type(l:vimjobinfo) == type({}) && has_key(l:vimjobinfo, 'process') 331 | return l:vimjobinfo['process'] 332 | endif 333 | endif 334 | return 0 335 | endfunction 336 | 337 | function! s:callback_cb(jobid, opts, ch, data) abort 338 | if has_key(a:opts, 'on_stdout') 339 | call a:opts.on_stdout(a:jobid, a:data, 'stdout') 340 | endif 341 | endfunction 342 | 343 | function! s:callback_cb_array(jobid, opts, ch, data) abort 344 | if has_key(a:opts, 'on_stdout') 345 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 346 | endif 347 | endfunction 348 | 349 | function! s:close_cb(jobid, opts, ch) abort 350 | if has_key(a:opts, 'on_exit') 351 | call a:opts.on_exit(a:jobid, 'closed', 'exit') 352 | endif 353 | if has_key(s:jobs, a:jobid) 354 | call remove(s:jobs, a:jobid) 355 | endif 356 | endfunction 357 | 358 | " public apis {{{ 359 | function! ripgrep#job#start(cmd, opts) abort 360 | return s:job_start(a:cmd, a:opts) 361 | endfunction 362 | 363 | function! ripgrep#job#stop(jobid) abort 364 | call s:job_stop(a:jobid) 365 | endfunction 366 | 367 | function! ripgrep#job#send(jobid, data, ...) abort 368 | let l:opts = get(a:000, 0, {}) 369 | call s:job_send(a:jobid, a:data, l:opts) 370 | endfunction 371 | 372 | function! ripgrep#job#wait(jobids, ...) abort 373 | let l:timeout = get(a:000, 0, -1) 374 | return s:job_wait(a:jobids, l:timeout) 375 | endfunction 376 | 377 | function! ripgrep#job#pid(jobid) abort 378 | return s:job_pid(a:jobid) 379 | endfunction 380 | 381 | function! ripgrep#job#connect(addr, opts) abort 382 | let s:jobidseq = s:jobidseq + 1 383 | let l:jobid = s:jobidseq 384 | let l:retry = 0 385 | let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw 386 | while l:retry < 5 387 | let l:ch = ch_open(a:addr, {'waittime': 1000}) 388 | call ch_setoptions(l:ch, { 389 | \ 'callback': function(l:normalize ==# 'array' ? 's:callback_cb_array' : 's:callback_cb', [l:jobid, a:opts]), 390 | \ 'close_cb': function('s:close_cb', [l:jobid, a:opts]), 391 | \ 'mode': 'raw', 392 | \}) 393 | if ch_status(l:ch) ==# 'open' 394 | break 395 | endif 396 | sleep 100m 397 | let l:retry += 1 398 | endwhile 399 | let s:jobs[l:jobid] = { 400 | \ 'type': s:job_type_vimjob, 401 | \ 'opts': a:opts, 402 | \ 'job': l:ch, 403 | \ 'channel': l:ch, 404 | \ 'buffer': '' 405 | \} 406 | return l:jobid 407 | endfunction 408 | " }}} 409 | 410 | let &cpo = s:save_cpo 411 | unlet s:save_cpo 412 | --------------------------------------------------------------------------------