diff --git a/README.md b/README.md index 62165eb..5013cf0 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,74 @@ MacVim! Tmux complete is automatically integrated with the following plugins: -- [neocomplete](https://github.com/Shougo/neocomplete.vim): You can see tmux - completions right in your neocomplete pop-up. +- [asyncomplete](https://github.com/prabirshrestha/asyncomplete.vim) -- [neocomplcache](https://github.com/Shougo/neocomplcache.vim): You can see tmux - completions right in your neocomplcache pop-up. + To see tmux completions in your asyncomplete pop-up you will need the async + plugin as well: -- [deoplete](https://github.com/Shougo/deoplete.nvim): You can see tmux - completions right in your deoplete pop-up. + ```vim + Plug 'prabirshrestha/async.vim' + Plug 'prabirshrestha/asyncomplete.vim' + Plug 'wellle/tmux-complete.vim' + ``` + + This integration comes with sensible defaults, but you have some options to + fine tune it. To start put a block like this into your vimrc: + + ```vim + let g:tmuxcomplete#asyncomplete_source_options = { + \ 'name': 'tmuxcomplete', + \ 'whitelist': ['*'], + \ 'config': { + \ 'splitmode': 'words', + \ 'filter_prefix': 1, + \ 'show_incomplete': 1, + \ 'sort_candidates': 0, + \ 'scrollback': 0 + \ } + \ } + ``` + + With `name` you can change how it appears in the pop-up. `whitelist` makes + it possible to enable this integration only for certain filetypes. + + The `splitmode` can be `words`, `lines`, `ilines`, or `linies,words`. + `ilines` stands for "inner line", starting with a word character (ignoring + special chararcters in front) and `ilines,words` completes both lines and + words. + + If `filter_prefix` is enabled, we will filter candidates based on the + entered text, this usually gives faster results. For fuzzy matching this + should be disabled. + + If there you are using many tmux windows with a lot of text in it, + completion can be slow. That's why we start showing candidates as soon as + they come in. If you prefer to only see candidates once the list is + complete, you can disable this by setting `show_incomplete`. + + `sort_candidates` controls whether we sort candidates from tmux externally. + If it's enabled we can't get early incomplete results. If you have + `show_incomplete` disabled, this might get slightly quicker results and + potentially better sorted completions. + + If `scrollback` is positive we will consider that many lines in each tmux + pane's history for completion. + +- [neocomplete](https://github.com/Shougo/neocomplete.vim) + + You can see tmux completions right in your neocomplete pop-up. + +- [neocomplcache](https://github.com/Shougo/neocomplcache.vim) -- [unite](https://github.com/Shougo/unite.vim): You can use tmux complete - as a unite source: + You can see tmux completions right in your neocomplcache pop-up. + +- [deoplete](https://github.com/Shougo/deoplete.nvim) + + You can see tmux completions right in your deoplete pop-up. + +- [unite](https://github.com/Shougo/unite.vim) + + You can use tmux complete as a unite source: ```vim Unite tmuxcomplete " opens a menu containing words from adjacent tmux windows @@ -57,22 +114,22 @@ Tmux complete is automatically integrated with the following plugins: Use your favorite plugin manager. -- [NeoBundle][neobundle] +- [Vim-plug][vim-plug] ```vim - NeoBundle 'wellle/tmux-complete.vim' + Plug 'wellle/tmux-complete.vim' ``` -- [Vundle][vundle] +- [NeoBundle][neobundle] ```vim - Bundle 'wellle/tmux-complete.vim' + NeoBundle 'wellle/tmux-complete.vim' ``` -- [Vim-plug][vim-plug] +- [Vundle][vundle] ```vim - Plug 'wellle/tmux-complete.vim' + Bundle 'wellle/tmux-complete.vim' ``` - [Pathogen][pathogen] diff --git a/autoload/asyncomplete/sources/tmuxcomplete.vim b/autoload/asyncomplete/sources/tmuxcomplete.vim new file mode 100644 index 0000000..4042262 --- /dev/null +++ b/autoload/asyncomplete/sources/tmuxcomplete.vim @@ -0,0 +1,113 @@ +let s:defaultopts = { + \ 'name': 'tmuxcomplete', + \ 'whitelist': ['*'], + \ 'completor': function('asyncomplete#sources#tmuxcomplete#completor'), + \ } + +" 'splitmode' can be 'words', 'lines', 'ilines', or 'linies,words' +" 'ilines' is inner line, starting with a word character (ignoring special +" chararcters in front) +" 'ilines,words' completes both lines and words +" more combinations can be added per request +" +" if 'filter_prefix' is enabled, we will filter candidates based on the entered +" text, this usually gives faster results. for fuzzy matching this should be +" disabled +" +" if there you are using many tmux windows with a lot of text in it completion +" can be slow. that's why we start showing candidates as soon as they come in +" if you prefer to only see candidates once the list is complete, you can +" disable this by setting 'show_incomplete' +" +" 'sort_candidates' controls whether we sort candidates from tmux externally. +" if it's enabled we can't get early incomplete results. if you have +" 'show_incomplete' disabled, this might get slightly quicker results and +" potentially better sorted completions. +" +" if 'scrollback' is positive we will consider that many lines in each tmux +" pane's history for completion +let s:defaultconfig = { + \ 'splitmode': 'words', + \ 'filter_prefix': 1, + \ 'show_incomplete': 1, + \ 'sort_candidates': 0, + \ 'scrollback': 0 + \ } + +function! asyncomplete#sources#tmuxcomplete#register(opts) + let l:opts = extend(copy(s:defaultopts), a:opts) + call asyncomplete#register_source(l:opts) +endfunction + +function! asyncomplete#sources#tmuxcomplete#completor(opt, ctx) + " taken from asyncomplete-buffer + let l:kw = matchstr(a:ctx.typed, '\w\+$') + let l:kwlen = len(l:kw) + if l:kwlen < 1 + return + endif + " echom '#completor for ' . l:kw + + let l:config = extend(copy(s:defaultconfig), get(a:opt, 'config', {})) + " echom 'config ' . string(l:config) + + let l:params = { + \ 'name': a:opt.name, + \ 'ctx': a:ctx, + \ 'startcol': a:ctx.col - l:kwlen, + \ 'config': l:config, + \ 'kw': l:kw, + \ 'raw': [], + \ 'mapped': [] + \ } + + " Add first empty candidate as incomplete to allow adding more + " completions later, even if the context changed in between. + call s:complete(l:params, [''], 1) + + if !l:config.filter_prefix + let l:kw = '' + endif + + let l:cmd = tmuxcomplete#getcommandlist(l:kw, l:config.scrollback, l:config.splitmode) + if !l:config.sort_candidates + call add(l:cmd, '-n') + endif + " echom 'cmd ' . string(l:cmd) + + let l:jobid = async#job#start(l:cmd, { + \ 'on_stdout': function('s:stdout', [l:params]), + \ 'on_exit': function('s:exit', [l:params]), + \ }) +endfunction + +function! s:stdout(params, id, data, event) abort + " echom '#stdout for ' . a:params.kw . ' with ' . (len(a:data) < 5 ? string(a:data) : string(a:data[0 : 1]) . ' ..' . len(a:data) . '.. ' . string(a:data[-2 : -1])) + + call extend(a:params.raw, a:data) " to be mapped differently again on exit + + if a:params.config.show_incomplete + " surround with pipes while incomplete + call extend(a:params.mapped, map(copy(a:data), '{"word":v:val,"menu":"|' . a:params.name . '|"}')) + call s:complete(a:params, a:params.mapped, 1) + endif +endfunction + +function! s:exit(params, id, data, event) abort + " echom '#exit for ' . a:params.kw . ' with ' . a:data + + if a:data != 0 " command failed + " echom 'failed with ' . a:data + " set candidates as complete to stop completing on context changes + call s:complete(a:params, [''], 0) + return + endif + + " surround with brackends when complete + let l:mapped = map(a:params.raw, '{"word":v:val,"menu":"[' . a:params.name . ']"}') + call s:complete(a:params, l:mapped, 0) +endfunction + +function! s:complete(params, candidates, incomplete) + call asyncomplete#complete(a:params.name, a:params.ctx, a:params.startcol, a:candidates, a:incomplete) +endfunction diff --git a/autoload/tmuxcomplete.vim b/autoload/tmuxcomplete.vim index 893afdc..5ece09a 100644 --- a/autoload/tmuxcomplete.vim +++ b/autoload/tmuxcomplete.vim @@ -13,33 +13,39 @@ endfunction let s:script = expand(':h:h') . "/sh/tmuxcomplete" -function! s:build_command(base, capture_args, splitmode) +function! s:build_command(base, capture_args, splitmode, as) let pattern = '^' . escape(a:base, '*^$][.\') . '.' let list_args = get(g:, 'tmuxcomplete#list_args', '-a') let grep_args = tmuxcomplete#grepargs(a:base) - let command = 'sh ' . shellescape(s:script) - let command .= ' -p ' . shellescape(pattern) - let command .= ' -s ' . shellescape(a:splitmode) - let command .= ' -l ' . shellescape(list_args) - let command .= ' -c ' . shellescape(a:capture_args) - let command .= ' -g ' . shellescape(grep_args) + let command = s:newcommand(a:as) + let command = s:addcommand2(command, a:as, 'sh', s:script) + let command = s:addcommand2(command, a:as, '-p', pattern) + let command = s:addcommand2(command, a:as, '-s', a:splitmode) + let command = s:addcommand2(command, a:as, '-l', list_args) + let command = s:addcommand2(command, a:as, '-c', a:capture_args) + let command = s:addcommand2(command, a:as, '-g', grep_args) - if $TMUX_PANE !=# "" " if running inside tmux - let command .= ' -e' " exclude current pane + if $TMUX_PANE !=# "" " if running inside tmux + let command = s:addcommand1(command, a:as, '-e') " exclude current pane endif return command endfunction function! tmuxcomplete#getcommand(base, splitmode) - return s:build_command(a:base, s:capture_args, a:splitmode) + return s:build_command(a:base, s:capture_args, a:splitmode, 'string') +endfunction + +function! tmuxcomplete#getcommandlist(base, scrollback, splitmode) + let capture_args = s:capture_args . ' -S -' . a:scrollback + return s:build_command(a:base, capture_args, a:splitmode, 'list') endfunction function! tmuxcomplete#completions(base, capture_args, splitmode) - let command = s:build_command(a:base, a:capture_args, a:splitmode) + let command = s:build_command(a:base, a:capture_args, a:splitmode, 'string') - let completions = system(command) + let completions = system(command) " TODO: use systemlist()? if v:shell_error != 0 return [] endif @@ -97,6 +103,31 @@ function! tmuxcomplete#grepargs(base) return '-i' endfunction +function! s:newcommand(as) + if a:as == 'list' + return [] + else " string + return '' + endif +endfunction + +function! s:addcommand1(command, as, value) + if a:as == 'list' + return add(a:command, a:value) + else " string + return (a:command == '' ? '' : a:command . ' ') . a:value + endif +endfunction + +function! s:addcommand2(command, as, key, value) + if a:as == 'list' + call add(a:command, a:key) + return add(a:command, a:value) " no escaping here + else " string + return (a:command == '' ? '' : a:command . ' ') . a:key . ' ' . shellescape(a:value) + endif +endfunction + " for integration with completion frameworks function! tmuxcomplete#gather_candidates() return tmuxcomplete#complete(0, '') diff --git a/plugin/tmuxcomplete.vim b/plugin/tmuxcomplete.vim index 0d93eb0..7c4884c 100644 --- a/plugin/tmuxcomplete.vim +++ b/plugin/tmuxcomplete.vim @@ -1,7 +1,7 @@ if exists("g:tmuxcomplete#loaded") || &cp || v:version < 700 finish endif -let g:tmuxcomplete#loaded = '0.1.1' " version number +let g:tmuxcomplete#loaded = '0.1.3' " version number function! s:init() let trigger = get(g:, 'tmuxcomplete#trigger', 'completefunc') @@ -15,6 +15,9 @@ function! s:init() else echoerr "tmux-complete: unknown trigger: '" . trigger . "'" endif + + let s:options = get(g:, 'tmuxcomplete#asyncomplete_source_options', {}) + autocmd User asyncomplete_setup call asyncomplete#sources#tmuxcomplete#register(s:options) endfunction call s:init() diff --git a/sh/tmuxcomplete b/sh/tmuxcomplete index b459176..857240b 100644 --- a/sh/tmuxcomplete +++ b/sh/tmuxcomplete @@ -34,14 +34,16 @@ if ! tmux info > /dev/null 2>&1; then fi EXCLUDE='0' +NOSORT='0' PATTERN='' SPLITMODE=words LISTARGS='' CAPTUREARGS='' GREPARGS='' -while getopts ep:s:l:c:g: name +while getopts enp:s:l:c:g: name do case $name in e) EXCLUDE="1";; + n) NOSORT="1";; # internal/undocumented, don't use, might be changed in the future p) PATTERN="$OPTARG";; s) SPLITMODE="$OPTARG";; l) LISTARGS="$OPTARG";; @@ -87,19 +89,62 @@ capturepanes() { } split() { - if [ "$SPLITMODE" = "lines" ]; then + if [ "$SPLITMODE" = "ilines,words" ]; then + # this is most reabable, but not posix compliant + # tee >(splitilines) >(splitwords) + + # from https://unix.stackexchange.com/a/43536 + # this has some issues with trailing whitespace sometimes + # tmp_dir=$(mktemp -d) + # mkfifo "$tmp_dir/f1" "$tmp_dir/f2" + # splitilines <"$tmp_dir/f1" & pid1=$! + # splitwords <"$tmp_dir/f2" & pid2=$! + # tee "$tmp_dir/f1" "$tmp_dir/f2" + # rm -rf "$tmp_dir" + # wait $pid1 $pid2 + + splitilinesandwords + elif [ "$SPLITMODE" = "lines" ]; then splitlines - else + elif [ "$SPLITMODE" = "ilines" ]; then + splitilines + elif [ "$SPLITMODE" = "words" ]; then splitwords fi } +splitilinesandwords() { + # print full line to duplicate it + # on the duplicate substitute all spaces with newlines + # duplicate that result again + # in that duplicate replace all non word characters by linebreaks + sed -e 'p;s/[[:space:]]\{1,\}/\ + /g;p;s/[^a-zA-Z0-9_]\{1,\}/\ + /g' | + # remove surrounding non-word characters + grep -o "\\w.*\\w" +} + splitlines() { # remove surrounding whitespace grep -o "\\S.*\\S" } +splitilines() { + # starts at first word character + grep -o "\\w.*\\S" +} + +# returns both WORDS and words of each given line splitwords() { + # use sed like this instead of tr? + # substitute all spaces with newlines + # duplicate that line + # in the duplicate replace all non word characters by linebreaks + # sed -e 's/[[:space:]]\{1,\}/\ + # /g;p;s/[^a-zA-Z0-9_]/ /g;s/[[:space:]]\{1,\}/\ + # /g' | + # copy lines and split words sed -e 'p;s/[^a-zA-Z0-9_]/ /g' | # split on spaces @@ -108,6 +153,14 @@ splitwords() { grep -o "\\w.*\\w" } +sortu() { + if [ "$NOSORT" = "1" ]; then + uniq + else + sort -u + fi +} + # list all panes listpanes | # filter out current pane