├── after └── plugin │ └── cmp_zsh.lua ├── bin ├── cmp_capture.zsh ├── cmp_capture_zshrc.zsh └── cmp_capture_shared.zsh ├── LICENSE ├── README.md └── lua └── cmp_zsh.lua /after/plugin/cmp_zsh.lua: -------------------------------------------------------------------------------- 1 | require('cmp').register_source('zsh', require('cmp_zsh').new()) 2 | -------------------------------------------------------------------------------- /bin/cmp_capture.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | zmodload zsh/zpty || { echo 'error: missing module zsh/zpty' >&2; exit 1 } 4 | 5 | # spawn shell 6 | zpty z zsh -f -i 7 | 8 | source ${0:h}/cmp_capture_shared.zsh 9 | -------------------------------------------------------------------------------- /bin/cmp_capture_zshrc.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | zmodload zsh/zpty || { echo 'error: missing module zsh/zpty' >&2; exit 1 } 4 | 5 | # spawn shell 6 | zpty z zsh -i 7 | 8 | # Disable history for commands starting with a space 9 | zpty -w z ' setopt histignorespace' 10 | 11 | # line buffer for pty output 12 | local line 13 | 14 | # swallow input of zshrc, disable hooks and disable PROMPT 15 | # the prompt should be disabled here (before init) in case a prompt theme has 16 | # a verbose prompt 17 | zpty -w z " autoload add-zsh-hook" 18 | zpty -w z " add-zsh-hook -D precmd '*'" 19 | zpty -w z " add-zsh-hook -D preexec '*'" 20 | zpty -w z " PROMPT=" 21 | zpty -w z " echo thisisalonganduniquestringtomarktheendoftheinit >/dev/null" 22 | zpty -r z line 23 | while [[ $line != *'thisisalonganduniquestringtomarktheendoftheinit'* ]]; do 24 | zpty -r z line 25 | done 26 | 27 | source ${0:h}/cmp_capture_shared.zsh 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 tamago324 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmp-zsh 2 | 3 | Zsh completion source for [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) 4 | 5 | ## Requirements 6 | 7 | * [Neovim](https://github.com/neovim/neovim/) 8 | * [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) 9 | * zsh/zpty module 10 | 11 | ```zsh 12 | zmodload zsh/zpty 13 | ``` 14 | 15 | ## Installation 16 | 17 | ```vim 18 | Plug 'hrsh7th/nvim-cmp' 19 | Plug 'tamago324/cmp-zsh' 20 | Plug 'Shougo/deol.nvim' " recommended to use together. 21 | 22 | lua << EOF 23 | require'cmp'.setup { 24 | -- ... 25 | sources = { 26 | { name = 'zsh' } 27 | } 28 | } 29 | EOF 30 | ``` 31 | 32 | ## Configuration 33 | 34 | It saves compdump file in `$CMP_ZSH_CACHE_DIR` or `$XDG_CACHE_HOME` or 35 | `$HOME/.cache` directory. 36 | 37 | In order to show completions defined in your zshrc you can setup cmp zsh like this. 38 | *Note*: This feature is experimental. If you enable it and don't get any 39 | results try disabling it. 40 | 41 | ```lua 42 | require'cmp_zsh'.setup { 43 | zshrc = true, -- Source the zshrc (adding all custom completions). default: false 44 | filetypes = { "deoledit", "zsh" } -- Filetypes to enable cmp_zsh source. default: {"*"} 45 | } 46 | ``` 47 | 48 | Alternatively you can use this trick (if sourcing your zshrc is very slow): 49 | Add the directory of the complete function to `$FPATH` in `~/.zshenv`. 50 | 51 | ```zsh 52 | # completions 53 | if [ -d $HOME/.zsh/comp ]; then 54 | export FPATH="$HOME/.zsh/comp:$FPATH" 55 | fi 56 | ``` 57 | 58 | 59 | ## Credit 60 | 61 | * [deoplete-zsh](https://github.com/deoplete-plugins/deoplete-zsh) 62 | * [zsh-capture-completion](https://github.com/Valodim/zsh-capture-completion) 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /lua/cmp_zsh.lua: -------------------------------------------------------------------------------- 1 | local cmp = require'cmp' 2 | local luv = require'luv' 3 | 4 | local script_abspath = function() 5 | -- これでこの関数を呼出したファイルの絶対パスが取得できる 6 | -- pprint(debug.getinfo(2, 'S').source:sub(2)) 7 | return debug.getinfo(2, 'S').source:sub(2) 8 | end 9 | local capture_script_path_base = script_abspath():match('(.*)/lua/cmp_zsh.lua$') .. '/bin/' 10 | 11 | local M = { 12 | config = { 13 | zshrc = false, 14 | filetypes = {"*"}, 15 | }, 16 | capture_script_path = capture_script_path_base .. 'cmp_capture.zsh' 17 | } 18 | 19 | M.setup = function(opts) 20 | opts = vim.tbl_deep_extend('keep', opts, M.config) 21 | M.config = opts 22 | 23 | local script = opts.zshrc and 'cmp_capture_zshrc.zsh' or 'cmp_capture.zsh' 24 | M.capture_script_path = capture_script_path_base .. script 25 | end 26 | 27 | M.new = function() 28 | local self = setmetatable({}, { __index = M }) 29 | return self 30 | end 31 | 32 | M.get_keyword_pattern = function() 33 | return [[\S\+]] 34 | end 35 | 36 | -- -- 利用可能かどうかを返す 37 | -- M.is_available = function() 38 | -- return vim.bo.filetype == 'zsh' 39 | -- end 40 | 41 | local line = function(str) 42 | str = str:gsub('\r', '') 43 | local s, e, cap = string.find(str, '\n') 44 | if not s then 45 | return nil, str 46 | end 47 | local l = string.sub(str, 1, s - 1) 48 | local rest = string.sub(str, e + 1) 49 | return l, rest 50 | end 51 | 52 | -- LSP の仕様に沿ったものを返す 53 | -- https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#completionItem 54 | -- TODO: documentation 55 | local result = function(words) 56 | local items = {} 57 | for _, v in ipairs(words) do 58 | -- table.insert(items, { label=v.label, documentation = v.documentation }) 59 | table.insert(items, { 60 | label = v.label, 61 | documentation = v.documentation, 62 | dup = 0, 63 | }) 64 | end 65 | return { items = items, isIncomplete = true } 66 | end 67 | 68 | local pipes = function() 69 | local stdin = luv.new_pipe(false) 70 | local stdout = luv.new_pipe(false) 71 | local stderr = luv.new_pipe(false) 72 | return { stdin, stdout, stderr } 73 | end 74 | 75 | M.complete = function(self, request, callback) 76 | -- local q = string.sub(request.context.cursor_before_line, request.offset) 77 | local q = request.context.cursor_before_line 78 | local stdioe = pipes() 79 | local handle, pid 80 | local buf = '' 81 | local words = {} 82 | 83 | do 84 | local spawn_params = { 85 | args = { self.capture_script_path, q }, 86 | stdio = stdioe 87 | } 88 | 89 | handle, pid = luv.spawn('zsh', spawn_params, function(code, signal) 90 | stdioe[1]:close() 91 | stdioe[2]:close() 92 | stdioe[3]:close() 93 | handle:close() 94 | vim.schedule_wrap(callback)(result(words)) 95 | end) 96 | 97 | if handle == nil then 98 | debug.log(string.format("start zsh failed: %s", pid)) 99 | end 100 | 101 | luv.read_start(stdioe[2], function(err, chunk) 102 | assert(not err, err) 103 | if chunk then 104 | buf = buf .. chunk 105 | end 106 | while true do 107 | local l, rest = line(buf) 108 | if l == nil then 109 | break 110 | end 111 | buf = rest 112 | local pieces = vim.split(l, ' -- ', true) 113 | if pieces[1] ~= "" then 114 | if #pieces > 1 then 115 | table.insert(words, {label = pieces[1], documentation = pieces[2]}) 116 | else 117 | table.insert(words, {label = pieces[1]}) 118 | end 119 | end 120 | end 121 | end) 122 | end 123 | end 124 | 125 | M.is_available = function(self) 126 | return vim.deep_equal(self.config.filetypes, {"*"}) or vim.tbl_contains(self.config.filetypes, vim.bo.filetype) 127 | end 128 | 129 | return M 130 | -------------------------------------------------------------------------------- /bin/cmp_capture_shared.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # line buffer for pty output 4 | local line 5 | 6 | setopt rcquotes 7 | () { 8 | zpty -w z " source $1" 9 | repeat 4; do 10 | zpty -r z line 11 | [[ $line == ok* ]] && return 12 | done 13 | echo 'error initializing.' >&2 14 | exit 2 15 | } =( <<< ' 16 | typeset -gx CMP_ZSH_CACHE_DIR=${CMP_ZSH_CACHE_DIR:-"${XDG_CACHE_HOME:-"$HOME/.cache"}/cmp/zsh"} 17 | 18 | mkdir -p "$CMP_ZSH_CACHE_DIR" 19 | 20 | # no prompt! 21 | PROMPT= 22 | 23 | # load completion system 24 | autoload -U compinit; compinit -C 25 | compinit -d "$CMP_ZSH_CACHE_DIR/compdump" 26 | 27 | # never run a command 28 | bindkey ''^M'' undefined 29 | bindkey ''^J'' undefined 30 | bindkey ''^I'' complete-word 31 | 32 | # send a line with null-byte at the end before and after completions are output 33 | null-line () { 34 | echo -E - $''\0'' 35 | } 36 | compprefuncs=( null-line ) 37 | comppostfuncs=( null-line exit ) 38 | 39 | # never group stuff! 40 | zstyle '':completion:*'' list-grouped false 41 | # don''t insert tab when attempting completion on empty line 42 | zstyle '':completion:*'' insert-tab false 43 | # no list separator, this saves some stripping later on 44 | zstyle '':completion:*'' list-separator '''' 45 | 46 | # we use zparseopts 47 | zmodload zsh/zutil 48 | 49 | # override compadd (this our hook) 50 | compadd () { 51 | 52 | # check if any of -O, -A or -D are given 53 | if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then 54 | # if that is the case, just delegate and leave 55 | builtin compadd "$@" 56 | return $? 57 | fi 58 | 59 | # ok, this concerns us! 60 | # echo -E - got this: "$@" 61 | 62 | # be careful with namespacing here, we don''t want to mess with stuff that 63 | # should be passed to compadd! 64 | typeset -a __hits __dscr __tmp 65 | 66 | # do we have a description parameter? 67 | # note we don''t use zparseopts here because of combined option parameters 68 | # with arguments like -default- confuse it. 69 | if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload 70 | # next param after -d 71 | __tmp=${@[$[${@[(i)-d]}+1]]} 72 | # description can be given as an array parameter name, or inline () array 73 | if [[ $__tmp == \(* ]]; then 74 | eval "__dscr=$__tmp" 75 | else 76 | __dscr=( "${(@P)__tmp}" ) 77 | fi 78 | fi 79 | 80 | # capture completions by injecting -A parameter into the compadd call. 81 | # this takes care of matching for us. 82 | builtin compadd -A __hits -D __dscr "$@" 83 | 84 | # JESUS CHRIST IT TOOK ME FOREVER TO FIGURE OUT THIS OPTION WAS SET AND WAS MESSING WITH MY SHIT HERE 85 | setopt localoptions norcexpandparam extendedglob 86 | 87 | # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool 88 | # -r remove-func magic, but it''s better than nothing. 89 | typeset -A apre hpre hsuf asuf 90 | zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf 91 | 92 | # append / to directories? we are only emulating -f in a half-assed way 93 | # here, but it''s better than nothing. 94 | integer dirsuf=0 95 | # don''t be fooled by -default- >.> 96 | if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then 97 | dirsuf=1 98 | fi 99 | 100 | # just drop 101 | [[ -n $__hits ]] || return 102 | 103 | # this is the point where we have all matches in $__hits and all 104 | # descriptions in $__dscr! 105 | 106 | # display all matches 107 | local dsuf dscr 108 | for i in {1..$#__hits}; do 109 | 110 | # add a dir suffix? 111 | (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf= 112 | # description to be displayed afterwards 113 | (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr= 114 | 115 | echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf$dscr 116 | 117 | done 118 | 119 | } 120 | 121 | # signal success! 122 | echo ok') 123 | 124 | zpty -w z "$*"$'\t' 125 | 126 | integer tog=0 127 | # read from the pty, and parse linewise 128 | while zpty -r z; do :; done | while IFS= read -r line; do 129 | if [[ $line == *$'\0\r' ]]; then 130 | (( tog++ )) && return 0 || continue 131 | fi 132 | # display between toggles 133 | (( tog )) && echo -E - $line 134 | done 135 | 136 | return 2 137 | --------------------------------------------------------------------------------