├── .gitignore ├── .vintrc.yaml ├── syntax └── kubernetes.vim ├── .github └── workflows │ └── vint.yml ├── LICENSE ├── plugin └── vimkubectl.vim ├── doc └── vimkubectl.txt ├── autoload ├── vimkubectl │ ├── util.vim │ ├── kube.vim │ └── buf.vim ├── vimkubectl.vim └── async │ └── job.vim └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /.vintrc.yaml: -------------------------------------------------------------------------------- 1 | cmdargs: 2 | # Checking more strictly 3 | severity: style_problem 4 | 5 | # Enable coloring 6 | color: true 7 | 8 | # Enable Neovim syntax 9 | env: 10 | neovim: true 11 | 12 | #policies: 13 | # Disable a violation 14 | #ProhibitSomethingEvil: 15 | # enabled: false 16 | 17 | # Enable a violation 18 | #ProhibitSomethingBad: 19 | # enabled: true 20 | -------------------------------------------------------------------------------- /syntax/kubernetes.vim: -------------------------------------------------------------------------------- 1 | if exists('b:current_syntax') 2 | finish 3 | endif 4 | 5 | syn match vkctlHeader '\v^[A-Z][a-z][^:]*: .*$' contains=vkctlIdentifier skipwhite 6 | syn match vkctlHelpHeader '\v^Help:' nextgroup=vkctlHelpTag skipwhite 7 | syn match vkctlResource '\v^[a-z \.]*\/[a-z \- 0-9 \.]*$' contains=vkctlResourcePrefix skipwhite 8 | 9 | syn match vkctlHelpTag '\v\S+' contained 10 | syn match vkctlResourcePrefix '\v^[a-z \.]*\/' contained 11 | syn match vkctlIdentifier '\v [a-z 0-9 ()]*$' contained 12 | 13 | hi def link vkctlHeader Label 14 | hi def link vkctlHelpHeader vkctlHeader 15 | hi def link vkctlIdentifier Function 16 | hi def link vkctlHelpTag Tag 17 | hi def link vkctlResourcePrefix Comment 18 | hi def link vkctlResource Identifier 19 | 20 | let b:current_syntax = 'vimkubectl' 21 | 22 | " vim: et:sw=2:sts=2: 23 | -------------------------------------------------------------------------------- /.github/workflows/vint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ '*' ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | vint: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Setup Pyhton 25 | uses: actions/setup-python@v2.2.2 26 | with: 27 | python-version: 3.7 28 | 29 | - name: Install dependencies 30 | run: pip install git+https://github.com/Vimjas/vint.git 31 | 32 | - name: Run vimscript linter 33 | run: vint . 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Mohammed Saud 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 | -------------------------------------------------------------------------------- /plugin/vimkubectl.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_vimkubectl') 2 | finish 3 | endif 4 | let g:loaded_vimkubectl = 1 5 | 6 | command -bar -bang -complete=custom,vimkubectl#allResources -nargs=? Kget call vimkubectl#openResourceListView() 7 | command -bar -bang -complete=custom,vimkubectl#allNamespaces -nargs=? Kns call vimkubectl#switchOrShowNamespace() 8 | command -bar -bang -complete=custom,vimkubectl#allContexts -nargs=? Kctx call vimkubectl#switchOrShowContext() 9 | command -bar -bang -complete=custom,vimkubectl#allResourcesAndObjects -nargs=+ Kedit call vimkubectl#editResourceObject() 10 | command -bar -bang -complete=custom,vimkubectl#allResources -nargs=+ Kdoc call vimkubectl#viewResourceDoc() 11 | command -bar -bang -nargs=0 -range=% Kapply ,call vimkubectl#buf#applyActiveBuffer() 12 | command -bar -nargs=+ K call vimkubectl#runCmd() 13 | 14 | augroup vimkubectl_internal 15 | autocmd! * 16 | autocmd BufReadCmd kube://* nested call vimkubectl#hijackBuffer() 17 | autocmd BufDelete kube://* nested call vimkubectl#cleanupBuffer(expand('')) 18 | autocmd BufReadCmd kubeDoc://* nested call vimkubectl#hijackDocBuffer() 19 | autocmd BufDelete kubeDoc://* nested call vimkubectl#cleanupBuffer(expand('')) 20 | augroup END 21 | 22 | " vim: et:sw=2:sts=2: 23 | -------------------------------------------------------------------------------- /doc/vimkubectl.txt: -------------------------------------------------------------------------------- 1 | *vimkubectl.txt* Manage Kubernetes resources from Vim 2 | 3 | INTRODUCTION *vimkubectl* 4 | 5 | This plugin defines a set of commands and mappings for viewing, editing and 6 | managing Kubernetes resources. 7 | 8 | This plugin assumes that the user is logged in to the Kubernetes cluster using 9 | `kubectl` or a similar command. 10 | 11 | COMMANDS *vimkubectl-commands* 12 | 13 | :Kget {resource} *:Kget* 14 | Open a split that displays a list of all resources that 15 | match {resource}. If {resource} is not given, pod is 16 | used. 17 | 18 | :Kedit {resource} {object} *:Kedit* 19 | Open a new split containing the manifest yaml of 20 | {object} of type {resource}. This is functionally 21 | similar to the `kubectl edit` command. (Also has 22 | `` completion) 23 | 24 | :Ksave {filename} *:Ksave* 25 | Save the currently open manifest locally to a file. 26 | If {filename} is not given, the resource name is used. 27 | (NOTE: This command is only available when viewing 28 | resource manifests) 29 | 30 | :Kns {name} *:Kns* 31 | Switch to using {name} as the current namespace. 32 | If {name} is not given, prints the currently selected 33 | namespace. 34 | (NOTE: This option only sets the namespace for 35 | |vimkubectl| commands, 36 | and does not set/modify `kubectl config`) 37 | 38 | :Kctx {name} *:Kctx* 39 | Switch to using {name} as the current context. 40 | If {name} is not given, prints the currently active context. 41 | 42 | :{range}Kapply *:Kapply* 43 | Apply file contents. When used with a 44 | selection({range}), applies the selected content, else 45 | applies the entire file. 46 | Can be used on any open buffer. 47 | 48 | :Kdoc {resource} *:Kdoc* 49 | Open a split that displays the manual for any Custom 50 | Resource Definition on the cluster. 51 | 52 | :K *:K* 53 | Run any arbitrary `kubectl` command. 54 | 55 | MAPPINGS *vimkubectl-mapping* 56 | 57 | These mappings can be used on the split buffers created by |:KGet| or the 58 | manifest buffers when editing a resource. 59 | 60 | ii *vimkubectl_ii* 61 | Open the resource under the cursor for editing, 62 | defaults to YAML format. 63 | 64 | is *vimkubectl_is* 65 | Open the resource in a new split. 66 | 67 | iv *vimkubectl_iv* 68 | Open the resource in a new vertical split. 69 | 70 | it *vimkubectl_it* 71 | Open the resource in a new tab. 72 | 73 | dd *vimkubectl_id* 74 | Delete the resource under the cursor. 75 | (Prompts for confirmation) 76 | 77 | gr *vimkubectl_gr* 78 | When used in the manifest edit buffer, 79 | Update the currently editing manifest. 80 | (NOTE: This will disregard any local unsaved changes) 81 | 82 | CONFIGURATION *vimkubectl-configuration* 83 | 84 | *g:vimkubectl_command* 85 | Vimkubectl uses the `kubectl` command for communicating with the cluster. Use 86 | the `g:vimkubectl_command` variable to specify a different command. 87 | For example, to use OpenShift's `oc` command: 88 | > 89 | let g:vimkubectl_command = 'oc' 90 | < 91 | *g:vimkubectl_timeout* 92 | The `g:vimkubectl_timeout` variable can be used to specify the amount of time 93 | (in seconds) to wait for responses, before considering the cluster won't 94 | return. The default timeout limit is `5` seconds. 95 | For example, to change the wait time to `10` seconds: 96 | > 97 | let g:vimkubectl_timeout = 10 98 | < 99 | 100 | LICENSE *vimkubectl-license* 101 | 102 | MIT 103 | 104 | ------------------------------------------------------------------------------ 105 | vim: ft=help:tw=78:noet:norl: 106 | -------------------------------------------------------------------------------- /autoload/vimkubectl/util.vim: -------------------------------------------------------------------------------- 1 | " UTILS 2 | " ----- 3 | const s:msgPrefix = '[Vimkubectl] ' 4 | 5 | " Clear all undo history 6 | " Adapted from: https://vi.stackexchange.com/a/16915/22360 7 | fun! vimkubectl#util#resetUndo(bufnr) abort 8 | try 9 | const undo_setting = getbufvar(a:bufnr, '&undolevels') 10 | call setbufvar(a:bufnr, '&undolevels', -1) 11 | call appendbufline(a:bufnr, '$', '') 12 | call deletebufline(a:bufnr, '$') 13 | finally 14 | call setbufvar(a:bufnr, '&undolevels', l:undo_setting) 15 | endtry 16 | endfun 17 | 18 | " Print a message to cmdline 19 | fun! vimkubectl#util#showMessage(message = '') abort 20 | echo s:msgPrefix . a:message 21 | endfun 22 | 23 | " Print a message to cmdline, and save to :messages history 24 | fun! vimkubectl#util#printMessage(message = '') abort 25 | echom s:msgPrefix . a:message 26 | endfun 27 | 28 | " Print a message with warning highlight, and save to :messages history 29 | fun! vimkubectl#util#printWarning(message = '') abort 30 | echohl WarningMsg | echom s:msgPrefix . a:message | echohl None 31 | endfun 32 | 33 | " Print a message with error highlight, and save to :messages history 34 | fun! vimkubectl#util#printError(message = '') abort 35 | echohl ErrorMsg | echom s:msgPrefix . a:message | echohl None 36 | endfun 37 | 38 | " Clear the cmd line 39 | " https://stackoverflow.com/a/33854736/7683374 40 | fun! vimkubectl#util#clearCmdLine() abort 41 | echon "\r\r" 42 | echon '' 43 | endfun 44 | 45 | " Save buffer file contents to local file 46 | " saves as `resourceType_resource.yaml` if name is not given 47 | fun! vimkubectl#util#saveToFile(fname = '') abort 48 | let fileName = a:fname 49 | if !len(a:fname) 50 | let l:fileName = substitute(substitute(expand('%'), '\v^kube:\/\/', '', ''), '\v\/', '_', '') . '.yaml' 51 | endif 52 | const manifest = getline('1', '$') 53 | call writefile(l:manifest, l:fileName) 54 | call vimkubectl#util#printMessage('Saved to ' . l:fileName) 55 | endfun 56 | 57 | " Wrapper over async.vim https://github.com/prabirshrestha/async.vim 58 | " Run the `cmd` asynchronously, and call `callback` ONLY once command has 59 | " exited. 60 | " written to(Does not run when STDOUT is empty). 61 | " Print error message in case of non-zero return. 62 | " 63 | " Note on outType: 64 | " `outType` defines the data type, either 'string'(default), 'array' or 'raw' 65 | " 'string' is noop in vim, 'array' is noop in nvim 66 | " 'raw' will mean array for nvim and string for vim 67 | fun! vimkubectl#util#asyncExec(cmd, callback, outType = 'string', ctx = {}) abort 68 | let outData = a:outType ==# 'array' ? [] : '' 69 | let handlers = { 'normalize': a:outType } 70 | 71 | fun! handlers.on_stdout(jobId, data, event) closure abort 72 | if !len(a:data) 73 | return 74 | endif 75 | " Combine last line & first line to avoid s between stdout callbacks. 76 | " TODO: issue still exists for 'string', 77 | " substitute()ing does not seem to fix this 78 | if a:outType ==# 'array' 79 | if len(l:outData) 80 | let l:outData[-1] .= a:data[0] 81 | else 82 | let l:outData = [a:data[0]] 83 | endif 84 | call extend(l:outData, a:data[1:]) 85 | else 86 | let l:outData .= a:data 87 | endif 88 | endfun 89 | 90 | fun! handlers.on_stderr(jobId, data, event) closure abort 91 | if len(a:data) > 0 92 | if a:outType ==# 'string' 93 | call vimkubectl#util#printError(a:data) 94 | else 95 | if len(a:data[0]) > 0 96 | call vimkubectl#util#printError(join(a:data, '\n')) 97 | endif 98 | endif 99 | endif 100 | endfun 101 | 102 | fun! handlers.on_exit(jobId, data, event) closure abort 103 | call a:callback(l:outData, a:ctx) 104 | endfun 105 | 106 | return async#job#start(a:cmd, handlers) 107 | endfun 108 | 109 | " Wrapper over async.vim https://github.com/prabirshrestha/async.vim 110 | " Same as vimkubectl#util#asyncExec but calls `callback` everytime STDOUT is 111 | " written to(Does not run when STDOUT is empty). 112 | fun! vimkubectl#util#asyncRun(cmd, callback, outType = 'string', ctx = {}) abort 113 | let handlers = { 'normalize': a:outType } 114 | 115 | fun! handlers.on_stdout(jobId, data, event) closure abort 116 | if len(a:data) 117 | call a:callback(a:data, a:ctx) 118 | endif 119 | endfun 120 | 121 | fun! handlers.on_stderr(jobId, data, event) closure abort 122 | if len(a:data) 123 | if a:outType ==# 'string' 124 | call vimkubectl#util#printError(a:data) 125 | else 126 | if len(a:data[0]) > 0 127 | call vimkubectl#util#printError(join(a:data, '\n')) 128 | endif 129 | endif 130 | endif 131 | endfun 132 | 133 | fun! handlers.on_exit(jobId, data, event) closure abort 134 | endfun 135 | 136 | return async#job#start(a:cmd, handlers) 137 | endfun 138 | 139 | " vim: et:sw=2:sts=2: 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vimkubectl 2 | ========== 3 | [![CI badge](https://github.com/rottencandy/vimkubectl/actions/workflows/vint.yml/badge.svg)](https://github.com/rottencandy/vimkubectl/actions/workflows/vint.yml) 4 | 5 | A Vim/Neovim plugin to manipulate Kubernetes resources. 6 | 7 | ![Screenshot of plugin in use](https://i.imgur.com/PwDD7pS.png) 8 | 9 | This plugin is a `kubectl` wrapper providing commands and mappings to perform actions on Kubernetes resources. 10 | 11 | The following has been implemented so far: 12 | - Fetch and view lists of kubernetes resources 13 | - Edit resource manifests in YAML form 14 | - Apply any file or buffer to the cluster 15 | - Delete resources 16 | - Switch namespace 17 | - Switch contexts 18 | - View kubernetes resource manuals 19 | 20 | Installation 21 | ------------ 22 | 23 | This plugin follows the standard runtime path structure, 24 | you can install it with your favourite plugin manager. 25 | 26 | Plugin Manager | Instructions 27 | --------------- | -------------------------------------------------- 28 | [NeoBundle][0] | `NeoBundle 'rottencandy/vimkubectl'` 29 | [Vundle][1] | `Plugin 'rottencandy/vimkubectl'` 30 | [Plug][2] | `Plug 'rottencandy/vimkubectl'` 31 | [Pathogen][3] | `git clone https://github.com/rottencandy/vimkubectl ~/.vim/bundle` 32 | Vim 8+ packages | `git clone git@github.com:rottencandy/vimkubectl.git ~/.vim/pack/vendor/start/vimkubectl && vim -u NONE -c "helptags ~/.vim/pack/vendor/start/vimkubectl/doc" -c q` 33 | 34 | Usage 35 | ----- 36 | 37 | This plugin assumes your Kubernetes cluster is reachable and logged in with [kubectl][4] or [oc][5].(see [configuration](#configuration)) 38 | 39 | - `:Kget {resource}` 40 | 41 | Get a list of all objects of type `{resource}`. If `{resource}` is not given, `pod` is used. 42 | 43 | You can also use `` for completion and to cycle through possible resources. 44 | 45 | - `ii` (think, "insert mode") to open and edit the manifest of the resource under cursor, in the current window(opens in `YAML` format) 46 | 47 | - `is` to open in a split. 48 | 49 | - `iv` to open in a vertical split. 50 | 51 | - `it` to open in a new tab. 52 | 53 | - `dd` to delete the resource under cursor. (Prompts for confirmation) 54 | 55 | - `:Kedit {resource} {object}` 56 | 57 | Open a split containing the manifest of `{object}` of type `{resource}`. Also has `` completion. 58 | 59 | - The opened manifest can be edited just like a regular file, except that it gets applied on every save. 60 | 61 | The following mappings are available in these buffers: 62 | 63 | - `gr` to refresh/update the manifest. Note that this will disregard any unsaved local changes. 64 | 65 | - `:Ksave {filename}` to save the manifest locally. If `{filename}` is not given, the resource object name is used. 66 | 67 | - `:Kns {name}` 68 | 69 | Change the active namespace to `{name}`. If `{name}` is not given, prints the currently used namespace. 70 | 71 | `` completion can be used to cycle through available namespaces. 72 | 73 | - `:Kctx {name}` 74 | 75 | Change the active context to `{name}`. If `{name}` is not given, prints the currently active context. 76 | 77 | `` completion can be used to cycle through available contexts. 78 | 79 | - `:{range}Kapply` 80 | 81 | Apply file contents. When used with a selection(), applies the selected content, else applies the entire file. 82 | Can be used on any open buffer. 83 | 84 | - `:Kdoc {resource}` 85 | 86 | Retrieve documentation on a specific `{resource}`. 87 | 88 | You can also use `` for completion and to cycle through possible resources that are manifest on your kubernetes cluster. 89 | 90 | > **_NOTE:_** `:Kdoc` will retrieve manuals for any CRD on the cluster. There doesn't have to be any existing k8s objects 91 | on the cluster in order to retrieve the manual. 92 | 93 | - `:K` 94 | 95 | Run any arbitrary `kubectl` command. 96 | 97 | Configuration 98 | ------------- 99 | 100 | - `g:vimkubectl_command` 101 | 102 | **Default: 'kubectl'** 103 | 104 | If you are using an alternate Kubernetes client, it can be specified with `g:vimkubectl_command`. 105 | 106 | For example to use OpenShift's `oc` as the command, add this to your `vimrc`: 107 | ``` 108 | let g:vimkubectl_command = 'oc' 109 | ``` 110 | 111 | - `g:vimkubectl_timeout` 112 | 113 | **Default: 5** 114 | 115 | The maximum time to wait for the cluster to respond to requests. 116 | 117 | For example, to change the wait time to `10` seconds: 118 | ``` 119 | let g:vimkubectl_timeout = 10 120 | ``` 121 | 122 | License 123 | ------- 124 | 125 | [MIT](LICENSE) 126 | 127 | [0]: https://github.com/Shougo/neobundle.vim 128 | [1]: https://github.com/gmarik/vundle 129 | [2]: https://github.com/junegunn/vim-plug 130 | [3]: https://github.com/tpope/vim-pathogen 131 | [4]: https://github.com/kubernetes/kubectl 132 | [5]: https://github.com/openshift/oc 133 | -------------------------------------------------------------------------------- /autoload/vimkubectl.vim: -------------------------------------------------------------------------------- 1 | " COMMAND FUNCTIONS 2 | " ----------------- 3 | 4 | " :K 5 | " Runs any arbitrary command 6 | fun! vimkubectl#runCmd(cmd) abort 7 | if len(a:cmd) 8 | call vimkubectl#util#showMessage("Running...") 9 | call vimkubectl#kube#runCmd( 10 | \ a:cmd, 11 | \ { out -> vimkubectl#util#printMessage(trim(out)) } 12 | \ ) 13 | endif 14 | endfun 15 | 16 | " :Kns 17 | " If `name` is provided, switch to using it as active namespace. 18 | " Else print currently active namespace. 19 | fun! vimkubectl#switchOrShowNamespace(name) abort 20 | if len(a:name) 21 | call vimkubectl#kube#setNs( 22 | \ a:name, 23 | \ { -> vimkubectl#util#printMessage('Switched to ' . a:name) } 24 | \ ) 25 | else 26 | call vimkubectl#util#printMessage( 27 | \ 'Active NS: ' . vimkubectl#kube#fetchActiveNamespace() 28 | \ ) 29 | endif 30 | endfun 31 | 32 | " :Kctx 33 | " If `name` is provided, switch to using it as active context. 34 | " Else print currently active context. 35 | fun! vimkubectl#switchOrShowContext(name) abort 36 | if len(a:name) 37 | call vimkubectl#kube#setContext( 38 | \ a:name, 39 | \ { -> vimkubectl#util#printMessage('Switched to ' . a:name) } 40 | \ ) 41 | else 42 | const ctx = vimkubectl#kube#fetchActiveContext() 43 | if v:shell_error !=# 0 44 | call vimkubectl#util#printError('Unable to fetch active context') 45 | else 46 | call vimkubectl#util#printMessage('Active context: ' . l:ctx) 47 | endif 48 | endif 49 | endfun 50 | 51 | " :Kget 52 | " Open or if already existing, switch to view buffer and load the list 53 | " of `res` resources. 54 | " If `res` is not provided, 'pods' is assumed. 55 | fun! vimkubectl#openResourceListView(res) abort 56 | " TODO: support multiple resource types simultaneously 57 | const resource = len(a:res) ? split(a:res)[0] : 'pods' 58 | 59 | call vimkubectl#buf#view_load(l:resource) 60 | endfun 61 | 62 | " :Kedit 63 | " Open an edit buffer with the resource manifest loaded 64 | " Args could either be in `res/obj` or `res obj` format 65 | fun! vimkubectl#editResourceObject(args) abort 66 | const fullResource = split(a:args, '\s\+') 67 | if len(fullResource) > 1 68 | const resource = l:fullResource 69 | else 70 | const resource = split(a:args, '/') 71 | endif 72 | "TODO: provide config option for default open method 73 | call vimkubectl#buf#edit_load('split', l:resource[0], l:resource[1]) 74 | endfun 75 | 76 | " :Kdoc 77 | " Open a buffer with the resource manual loaded 78 | " Args need to be in `res` or `res.spec` format. 79 | " Args directly supplied to kubectl/oc explain 80 | fun! vimkubectl#viewResourceDoc(args) abort 81 | const resourceSpec = a:args 82 | call vimkubectl#buf#doc_load('split', l:resourceSpec) 83 | endfun 84 | 85 | " This one is determining if user is doing a Kedit or Kget 86 | fun! vimkubectl#hijackBuffer() abort 87 | const resource = substitute(expand('%'), '^kube://', '', '') 88 | const parsedResource = split(l:resource, '/') 89 | if len(parsedResource) ==# 1 90 | call vimkubectl#buf#view_prepare() 91 | else 92 | call vimkubectl#buf#edit_prepare() 93 | endif 94 | endfun 95 | 96 | fun! vimkubectl#hijackDocBuffer() abort 97 | call vimkubectl#buf#doc_prepare() 98 | endfun 99 | 100 | fun! vimkubectl#cleanupBuffer(buf) abort 101 | call vimkubectl#buf#view_cleanup() 102 | endfun 103 | 104 | " COMPLETION FUNCTIONS 105 | " -------------------- 106 | 107 | " Completion function for namespaces 108 | fun! vimkubectl#allNamespaces(A, L, P) abort 109 | const namespaces = vimkubectl#kube#fetchNamespaces() 110 | if v:shell_error !=# 0 111 | return '' 112 | endif 113 | return l:namespaces 114 | endfun 115 | 116 | " Completion function for contexts 117 | fun! vimkubectl#allContexts(A, L, P) abort 118 | const contexts = vimkubectl#kube#fetchContexts() 119 | if v:shell_error !=# 0 120 | return '' 121 | endif 122 | return l:contexts 123 | endfun 124 | 125 | " Completion function for resource types only 126 | fun! vimkubectl#allResources(A, L, P) abort 127 | const availableResources = vimkubectl#kube#fetchResourceTypes() 128 | if v:shell_error !=# 0 129 | return '' 130 | endif 131 | return l:availableResources 132 | endfun 133 | 134 | " Completion function for resource types and resource objects 135 | function! vimkubectl#allResourcesAndObjects(arg, line, pos) abort 136 | const arguments = split(a:line, '\s\+') 137 | if len(arguments) > 2 || len(arguments) > 1 && a:arg =~# '^\s*$' 138 | const objectList = vimkubectl#kube#fetchPureResourceList( 139 | \ arguments[1], 140 | \ vimkubectl#kube#fetchActiveNamespace() 141 | \ ) 142 | else 143 | const objectList = vimkubectl#kube#fetchResourceTypes() 144 | endif 145 | if v:shell_error !=# 0 146 | return '' 147 | endif 148 | return l:objectList 149 | endfunction 150 | 151 | " vim: et:sw=2:sts=2: 152 | -------------------------------------------------------------------------------- /autoload/vimkubectl/kube.vim: -------------------------------------------------------------------------------- 1 | " LEGACY SYNCHRONOUS FUNCTIONS 2 | " Check exit code for failure after calling these functions 3 | " TODO: migrate these to async api 4 | " ---------------------------- 5 | 6 | " Fetch list of all namespaces 7 | " returns string of space-separated values 8 | fun! vimkubectl#kube#fetchNamespaces() abort 9 | return system(s:craftCmd('get ns -o custom-columns=":metadata.name"')) 10 | endfun 11 | 12 | " Fetch list of resource types 13 | " Note: This uses --cached so mostly doesn't fail 14 | " returns string of space-separated values 15 | fun! vimkubectl#kube#fetchResourceTypes() abort 16 | return system(s:craftCmd(join(['api-resources', '--cached', '-o name']))) 17 | endfun 18 | 19 | " Same as above but returns only list of `resourceName` 20 | " returns string of space-separated values 21 | fun! vimkubectl#kube#fetchPureResourceList(resourceType, namespace) abort 22 | return system(s:craftCmd( 23 | \ join(['get', a:resourceType, '-o custom-columns=":metadata.name"']), 24 | \ a:namespace 25 | \ )) 26 | endfun 27 | 28 | " Get currently active namespace 29 | fun! vimkubectl#kube#fetchActiveNamespace() abort 30 | return system( 31 | \ s:craftCmd('config view --minify -o jsonpath=''{..namespace}''') 32 | \ ) 33 | endfun 34 | 35 | " Get currently active context 36 | fun! vimkubectl#kube#fetchActiveContext() abort 37 | return system( 38 | \ s:craftCmd('config current-context') 39 | \ ) 40 | endfun 41 | 42 | " Fetch all contexts 43 | " returns string of space-separated values 44 | fun! vimkubectl#kube#fetchContexts() abort 45 | return system( 46 | \ s:craftCmd('config get-contexts -o name') 47 | \ ) 48 | endfun 49 | 50 | " ASYNCHRONOUS FUNCTIONS 51 | " ---------------------- 52 | 53 | " Create command using g:vimkubectl_command 54 | fun! s:craftCmd(command, namespace = '') abort 55 | let nsFlag = len(a:namespace) ? '-n ' . a:namespace : '' 56 | let timeoutFlag = 57 | \ '--request-timeout=' 58 | \ . get(g:, 'vimkubectl_timeout', 5) 59 | \ . 's' 60 | return join([ 61 | \ get(g:, 'vimkubectl_command', 'kubectl'), 62 | \ a:command, 63 | \ l:nsFlag, 64 | \ l:timeoutFlag 65 | \ ]) 66 | endfun 67 | 68 | fun! s:asyncCmd(command) abort 69 | return ['bash', '-c', a:command] 70 | endfun 71 | 72 | fun! s:asyncLoopCmd(command, interval = 5) abort 73 | return [ 74 | \ 'bash', 75 | \ '-c', 76 | \ 'while true; do ' . a:command . '; sleep ' . a:interval . '; done' 77 | \ ] 78 | endfun 79 | 80 | " Fetch manifest of resource 81 | " callback gets array of strings of each line 82 | fun! vimkubectl#kube#fetchResourceManifest( 83 | \ resourceType, 84 | \ resource, 85 | \ namespace, 86 | \ callback 87 | \ ) abort 88 | let cmd = s:craftCmd( 89 | \ join(['get', a:resourceType, a:resource, '-o yaml']), 90 | \ a:namespace 91 | \ ) 92 | return vimkubectl#util#asyncExec(s:asyncCmd(l:cmd), a:callback, 'array') 93 | endfun 94 | 95 | " Apply string 96 | fun! vimkubectl#kube#applyString(stringData, onApply) abort 97 | let cmd = 'echo "$1" | ' . s:craftCmd('apply -f -') 98 | " arg 2 sets $0, name of shell 99 | " arg 3 stringData is supplied to cmd as $1 by bash 100 | " See bash(1) 101 | return vimkubectl#util#asyncExec( 102 | \ ['bash', '-c', l:cmd, 'apply', a:stringData], 103 | \ a:onApply 104 | \ ) 105 | endfun 106 | 107 | " Delete resource 108 | fun! vimkubectl#kube#deleteResource(resType, res, ns, onDel) abort 109 | let cmd = s:craftCmd(join(['delete', a:resType, a:res]), a:ns) 110 | return vimkubectl#util#asyncExec(s:asyncCmd(l:cmd), a:onDel) 111 | endfun 112 | 113 | " Set active context 114 | fun! vimkubectl#kube#setContext(ctx, onSet) abort 115 | const cmd = s:craftCmd('config use-context ' . a:ctx) 116 | return vimkubectl#util#asyncExec(s:asyncCmd(l:cmd), a:onSet) 117 | endfun 118 | 119 | " Set active namespace for current context 120 | fun! vimkubectl#kube#setNs(ns, onSet) abort 121 | const cmd = s:craftCmd('config set-context --current --namespace=' . a:ns) 122 | return vimkubectl#util#asyncExec(s:asyncCmd(l:cmd), a:onSet) 123 | endfun 124 | 125 | " Fetch list of resources of a given type 126 | " returns array of `resourceType/resourceName` 127 | fun! vimkubectl#kube#fetchResourceList( 128 | \ resourceType, 129 | \ namespace, 130 | \ callback, 131 | \ ctx = {} 132 | \ ) abort 133 | const cmd = s:craftCmd(join(['get', a:resourceType, '-o name']), a:namespace) 134 | return vimkubectl#util#asyncExec( 135 | \ s:asyncCmd(l:cmd), 136 | \ a:callback, 137 | \ 'array', 138 | \ a:ctx 139 | \ ) 140 | endfun 141 | 142 | " Same as fetchResourceList but keep polling every 5 seconds 143 | fun! vimkubectl#kube#fetchResourceListLoop( 144 | \ resourceType, 145 | \ namespace, 146 | \ callback, 147 | \ ctx = {} 148 | \ ) abort 149 | const cmd = s:craftCmd(join(['get', a:resourceType, '-o name']), a:namespace) 150 | return vimkubectl#util#asyncRun( 151 | \ s:asyncLoopCmd(l:cmd), 152 | \ a:callback, 153 | \ 'array', 154 | \ a:ctx 155 | \ ) 156 | endfun 157 | 158 | " Fetch doc 159 | " returns doc 160 | fun! vimkubectl#kube#fetchDoc( 161 | \ resourceSpec, 162 | \ namespace, 163 | \ callback, 164 | \ ctx = {} 165 | \ ) abort 166 | const cmd = s:craftCmd(join(['explain', a:resourceSpec ]), a:namespace) 167 | return vimkubectl#util#asyncRun( 168 | \ s:asyncCmd(l:cmd), 169 | \ a:callback, 170 | \ 'array', 171 | \ a:ctx 172 | \ ) 173 | endfun 174 | 175 | 176 | 177 | " Runs arbitrary command 178 | fun! vimkubectl#kube#runCmd(cmd, callback) abort 179 | const cmd = s:craftCmd(a:cmd) 180 | return vimkubectl#util#asyncExec(s:asyncCmd(l:cmd), a:callback) 181 | endfun 182 | 183 | " vim: et:sw=2:sts=2: 184 | -------------------------------------------------------------------------------- /autoload/vimkubectl/buf.vim: -------------------------------------------------------------------------------- 1 | let b:vimkubectl_jobid = 0 2 | 3 | fun! s:headerText(resource, resourceCount, ns) abort 4 | return [ 5 | \ 'Namespace: ' . a:ns, 6 | \ 'Resource: ' . a:resource . ' (' . a:resourceCount . ')', 7 | \ 'Help: g?', 8 | \ '', 9 | \ ] 10 | endfun 11 | 12 | " Get the resource under cursor line, 13 | " If cursor is on header, or blank space, return '' 14 | " TODO: range support 15 | fun! s:resourceUnderCursor() abort 16 | const headerLength = len(s:headerText('', 0, '')) 17 | if getpos('.')[1] <=# l:headerLength 18 | return '' 19 | endif 20 | const resource = split(getline('.')) 21 | if len(l:resource) 22 | return l:resource[0] 23 | endif 24 | return '' 25 | endfun 26 | 27 | " Open edit buffer for resource under cursor, 28 | " `opemMethod` can be one of [edit, sp, vs, tabe] 29 | fun! s:editResource(openMethod) abort 30 | const fullResource = s:resourceUnderCursor() 31 | if !len(l:fullResource) 32 | return 33 | endif 34 | 35 | const resource = split(l:fullResource, '/') 36 | call vimkubectl#buf#edit_load(a:openMethod, l:resource[0], l:resource[1]) 37 | endfun 38 | 39 | " Delete the resource under cursor, after confirmation prompt 40 | fun! s:deleteResource() abort 41 | const fullResource = s:resourceUnderCursor() 42 | const resource = split(l:fullResource, '/') 43 | if len(l:resource) !=# 2 44 | return 45 | endif 46 | 47 | const choice = confirm( 48 | \ 'Are you sure you want to delete ' . l:resource[1] . ' ?', "&Yes\n&No" 49 | \ ) 50 | if l:choice !=# 1 51 | return 52 | endif 53 | 54 | const ns = vimkubectl#kube#fetchActiveNamespace() 55 | if v:shell_error !=# 0 56 | call vimkubectl#util#printError('Unable to fetch active namespace.') 57 | return 58 | endif 59 | 60 | call vimkubectl#util#showMessage('Deleting...') 61 | call vimkubectl#kube#deleteResource( 62 | \ l:resource[0], 63 | \ l:resource[1], 64 | \ l:ns, 65 | \ { data -> vimkubectl#util#printMessage(trim(data)) 66 | \ }) 67 | endfun 68 | 69 | fun! s:refresh(data, ctx) abort 70 | call filter(a:data, { _, v -> len(v) }) 71 | "if len(a:data) ==# 0 72 | " call vimkubectl#util#printError('No ' . a:ctx.resourceType . ' found') 73 | " return 74 | "endif 75 | 76 | const header = s:headerText(a:ctx.resourceType, len(a:data), a:ctx.ns) 77 | 78 | " Clear the "loading" message 79 | "echo '' 80 | 81 | const winid = bufwinid(a:ctx.bufnr) 82 | const curpos = getcurpos(l:winid) 83 | " todo: is winsaveview and winrestview needed here too? 84 | 85 | call setbufvar(a:ctx.bufnr, '&modifiable', 1) 86 | call setbufline(a:ctx.bufnr, 1, l:header) 87 | call deletebufline(a:ctx.bufnr, len(l:header) + 1, '$') 88 | call appendbufline(a:ctx.bufnr, '$', a:data) 89 | call vimkubectl#util#resetUndo(a:ctx.bufnr) 90 | call setbufvar(a:ctx.bufnr, '&modifiable', 0) 91 | " restore cursor pos in (possibly) inactive buffer 92 | " https://github.com/vim/vim/issues/7784#issuecomment-774298015 93 | " todo: extract this to util 94 | call win_execute(l:winid, 'call setpos(".", l:curpos)') 95 | endfun 96 | 97 | fun! vimkubectl#buf#view_prepare() abort 98 | if exists('b:vimkubectl_prepared') 99 | return 100 | endif 101 | call vimkubectl#util#showMessage('Loading...') 102 | 103 | setlocal buftype=nowrite 104 | setlocal bufhidden=delete 105 | setlocal filetype=kubernetes 106 | setlocal noswapfile 107 | 108 | nnoremap g? :help vimkubectl-mapping 109 | nnoremap ii :call editResource('edit') 110 | nnoremap is :call editResource('sp') 111 | nnoremap iv :call editResource('vs') 112 | nnoremap it :call editResource('tabe') 113 | nnoremap dd :call deleteResource() 114 | 115 | " TODO: remove buffer if initial population fails 116 | const ns = vimkubectl#kube#fetchActiveNamespace() 117 | if v:shell_error !=# 0 118 | call vimkubectl#util#printError('Unable to fetch active namespace.') 119 | return 120 | endif 121 | 122 | const resourceType = substitute(expand('%'), '^kube://', '', '') 123 | const ctx = { 124 | \ 'bufnr': bufnr(), 125 | \ 'resourceType': l:resourceType, 126 | \ 'ns': l:ns, 127 | \ } 128 | 129 | let b:vimkubectl_prepared = 1 130 | let b:vimkubectl_jobid = vimkubectl#kube#fetchResourceListLoop( 131 | \ l:resourceType, 132 | \ l:ns, 133 | \ function('s:refresh'), 134 | \ l:ctx 135 | \ ) 136 | endfun 137 | 138 | fun! vimkubectl#buf#doc_prepare() abort 139 | if exists('b:vimkubectl_prepared') 140 | return 141 | endif 142 | call vimkubectl#util#showMessage('Loading...') 143 | 144 | setlocal buftype=nowrite 145 | setlocal bufhidden=delete 146 | setlocal filetype=text 147 | setlocal noswapfile 148 | 149 | nnoremap g? :help vimkubectl-mapping 150 | 151 | " TODO: remove buffer if initial population fails 152 | const ns = vimkubectl#kube#fetchActiveNamespace() 153 | if v:shell_error !=# 0 154 | call vimkubectl#util#printError('Unable to fetch active namespace.') 155 | return 156 | endif 157 | 158 | const resourceSpec = substitute(expand('%'), '^kubeDoc://', '', '') 159 | const ctx = { 160 | \ 'bufnr': bufnr(), 161 | \ 'resourceType': l:resourceSpec, 162 | \ 'ns': l:ns, 163 | \ } 164 | 165 | let b:vimkubectl_prepared = 1 166 | let b:vimkubectl_jobid = vimkubectl#kube#fetchDoc( 167 | \ l:resourceSpec, 168 | \ l:ns, 169 | \ function('s:refresh'), 170 | \ l:ctx 171 | \ ) 172 | endfun 173 | 174 | 175 | 176 | 177 | fun! s:fillBuffer(bufnr, data) abort 178 | if len(a:data) <=# 1 179 | return 180 | endif 181 | const winid = bufwinid(a:bufnr) 182 | const curpos = getcurpos(l:winid) 183 | call deletebufline(a:bufnr, 1, '$') 184 | call setbufline(a:bufnr, 1, a:data) 185 | call vimkubectl#util#resetUndo(a:bufnr) 186 | call setbufvar(a:bufnr, '&modified', 0) 187 | call win_execute(l:winid, 'call setpos(".", l:curpos)') 188 | endfun 189 | 190 | " Fetch the manifest of the resource and fill up the buffer, 191 | " after discarding any existing content 192 | fun! s:refreshEditBuffer() abort 193 | const fullResource = substitute(expand('%'), '^kube://', '', '') 194 | const resource = split(l:fullResource, '/') 195 | 196 | const ns = vimkubectl#kube#fetchActiveNamespace() 197 | if v:shell_error !=# 0 198 | call vimkubectl#util#printError('Unable to fetch active namespace.') 199 | return 200 | endif 201 | 202 | call vimkubectl#util#showMessage('Fetching manifest...') 203 | return vimkubectl#kube#fetchResourceManifest( 204 | \ l:resource[0], 205 | \ l:resource[1], 206 | \ l:ns, 207 | \ { data -> s:fillBuffer(bufnr(), data) } 208 | \ ) 209 | endfun 210 | 211 | " Apply the buffer contents 212 | " If range is used, apply only the selected section, 213 | " else apply entire buffer 214 | fun! vimkubectl#buf#applyActiveBuffer() range abort 215 | call vimkubectl#util#showMessage('Applying...') 216 | 217 | " todo: use shellescape? 218 | const manifest = join(getline(a:firstline, a:lastline), "\n") 219 | return vimkubectl#kube#applyString( 220 | \ l:manifest, 221 | \ { result -> vimkubectl#util#showMessage(trim(result)) } 222 | \ ) 223 | endfun 224 | 225 | fun! s:applyAndUpdate() abort 226 | call vimkubectl#util#showMessage('Applying...') 227 | 228 | fun! s:onApply(result, ...) abort 229 | call vimkubectl#util#showMessage(trim(a:result) . ' Updating manifest...') 230 | call s:refreshEditBuffer() 231 | endfun 232 | 233 | const manifest = join(getline(1, '$'), "\n") 234 | return vimkubectl#kube#applyString( 235 | \ l:manifest, 236 | \ function('s:onApply') 237 | \ ) 238 | endfun 239 | 240 | fun! vimkubectl#buf#edit_prepare() abort 241 | if exists('b:vimkubectl_prepared') 242 | return 243 | endif 244 | call vimkubectl#util#showMessage('Loading...') 245 | 246 | setlocal buftype=acwrite 247 | setlocal bufhidden=delete 248 | setlocal filetype=yaml 249 | setlocal noswapfile 250 | 251 | " TODO warn before redrawing with unsaved changes 252 | nnoremap gr :call refreshEditBuffer() 253 | command! -buffer -bar -bang -nargs=? -complete=file Ksave :call vimkubectl#util#saveToFile() 254 | 255 | augroup vimkubectl_internal_editBufferOnSave 256 | autocmd! * 257 | autocmd BufWriteCmd call applyAndUpdate() 258 | augroup END 259 | 260 | let b:vimkubectl_prepared = 1 261 | return s:refreshEditBuffer() 262 | endfun 263 | 264 | " Create or switch to view buffer(kube://{resourceType}) 265 | fun! vimkubectl#buf#view_load(resourceType) abort 266 | const existing = bufwinnr('^kube://' . a:resourceType . '$') 267 | if l:existing ==# -1 268 | execute 'split kube://' . a:resourceType 269 | else 270 | execute l:existing . 'wincmd w' 271 | endif 272 | endfun 273 | 274 | " Create or switch to edit buffer(kube://{resourceType}/{resourceName}) 275 | fun! vimkubectl#buf#edit_load(openMethod, resourceType, resourceName) abort 276 | " TODO verify if openMethod is valid 277 | const existing = bufwinnr('^kube://' . a:resourceType . '/' . a:resourceName . '$') 278 | if l:existing ==# -1 279 | silent! exec a:openMethod . ' kube://' . a:resourceType . '/' . a:resourceName 280 | else 281 | silent! execute l:existing . 'wincmd w' 282 | " refresh needs to be done explicitly because buffer override will not 283 | " happen to exising buffers (due to BufReadCmd) 284 | call s:refreshEditBuffer() 285 | endif 286 | endfun 287 | 288 | " Create or switch to doc buffer(kube://{resourceSpec}) 289 | fun! vimkubectl#buf#doc_load(openMethod, resourceSpec) abort 290 | " TODO verify if openMethod is valid 291 | const existing = bufwinnr('^kubeDoc://' . a:resourceSpec . '$') 292 | if l:existing ==# -1 293 | execute a:openMethod . ' kubeDoc://' . a:resourceSpec 294 | else 295 | execute l:existing . 'wincmd w' 296 | endif 297 | endfun 298 | 299 | fun! vimkubectl#buf#view_cleanup() abort 300 | const jid = get(b:, 'vimkubectl_jobid') 301 | if l:jid 302 | call async#job#stop(b:vimkubectl_jobid) 303 | let b:vimkubectl_jobid = 0 304 | endif 305 | endfun 306 | 307 | " vim: et:sw=2:sts=2: 308 | -------------------------------------------------------------------------------- /autoload/async/job.vim: -------------------------------------------------------------------------------- 1 | " Author: Prabir Shrestha 2 | " Website: https://github.com/prabirshrestha/async.vim 3 | " License: The MIT License {{{ 4 | " The MIT License (MIT) 5 | " 6 | " Copyright (c) 2016 Prabir Shrestha 7 | " 8 | " Permission is hereby granted, free of charge, to any person obtaining a copy 9 | " of this software and associated documentation files (the "Software"), to deal 10 | " in the Software without restriction, including without limitation the rights 11 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | " copies of the Software, and to permit persons to whom the Software is 13 | " furnished to do so, subject to the following conditions: 14 | " 15 | " The above copyright notice and this permission notice shall be included in all 16 | " copies or substantial portions of the Software. 17 | " 18 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | " SOFTWARE. 25 | " }}} 26 | 27 | let s:save_cpo = &cpoptions 28 | set cpoptions&vim 29 | 30 | let s:jobidseq = 0 31 | let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'} 32 | let s:job_type_nvimjob = 'nvimjob' 33 | let s:job_type_vimjob = 'vimjob' 34 | let s:job_error_unsupported_job_type = -2 " unsupported job type 35 | 36 | function! s:noop(...) abort 37 | endfunction 38 | 39 | function! s:job_supported_types() abort 40 | let l:supported_types = [] 41 | if has('nvim') 42 | let l:supported_types += [s:job_type_nvimjob] 43 | endif 44 | if !has('nvim') && has('job') && has('channel') && has('lambda') 45 | let l:supported_types += [s:job_type_vimjob] 46 | endif 47 | return l:supported_types 48 | endfunction 49 | 50 | function! s:job_supports_type(type) abort 51 | return index(s:job_supported_types(), a:type) >= 0 52 | endfunction 53 | 54 | function! s:out_cb(jobid, opts, job, data) abort 55 | call a:opts.on_stdout(a:jobid, a:data, 'stdout') 56 | endfunction 57 | 58 | function! s:out_cb_array(jobid, opts, job, data) abort 59 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 60 | endfunction 61 | 62 | function! s:err_cb(jobid, opts, job, data) abort 63 | call a:opts.on_stderr(a:jobid, a:data, 'stderr') 64 | endfunction 65 | 66 | function! s:err_cb_array(jobid, opts, job, data) abort 67 | call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr') 68 | endfunction 69 | 70 | function! s:exit_cb(jobid, opts, job, status) abort 71 | if has_key(a:opts, 'on_exit') 72 | call a:opts.on_exit(a:jobid, a:status, 'exit') 73 | endif 74 | if has_key(s:jobs, a:jobid) 75 | call remove(s:jobs, a:jobid) 76 | endif 77 | endfunction 78 | 79 | function! s:on_stdout(jobid, data, event) abort 80 | let l:jobinfo = s:jobs[a:jobid] 81 | call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event) 82 | endfunction 83 | 84 | function! s:on_stdout_string(jobid, data, event) abort 85 | let l:jobinfo = s:jobs[a:jobid] 86 | call l:jobinfo.opts.on_stdout(a:jobid, join(a:data, "\n"), a:event) 87 | endfunction 88 | 89 | function! s:on_stderr(jobid, data, event) abort 90 | let l:jobinfo = s:jobs[a:jobid] 91 | call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event) 92 | endfunction 93 | 94 | function! s:on_stderr_string(jobid, data, event) abort 95 | let l:jobinfo = s:jobs[a:jobid] 96 | call l:jobinfo.opts.on_stderr(a:jobid, join(a:data, "\n"), a:event) 97 | endfunction 98 | 99 | function! s:on_exit(jobid, status, event) abort 100 | if has_key(s:jobs, a:jobid) 101 | let l:jobinfo = s:jobs[a:jobid] 102 | if has_key(l:jobinfo.opts, 'on_exit') 103 | call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event) 104 | endif 105 | if has_key(s:jobs, a:jobid) 106 | call remove(s:jobs, a:jobid) 107 | endif 108 | endif 109 | endfunction 110 | 111 | function! s:job_start(cmd, opts) abort 112 | let l:jobtypes = s:job_supported_types() 113 | let l:jobtype = '' 114 | 115 | if has_key(a:opts, 'type') 116 | if type(a:opts.type) == type('') 117 | if !s:job_supports_type(a:opts.type) 118 | return s:job_error_unsupported_job_type 119 | endif 120 | let l:jobtype = a:opts.type 121 | else 122 | let l:jobtypes = a:opts.type 123 | endif 124 | endif 125 | 126 | if empty(l:jobtype) 127 | " find the best jobtype 128 | for l:jobtype2 in l:jobtypes 129 | if s:job_supports_type(l:jobtype2) 130 | let l:jobtype = l:jobtype2 131 | endif 132 | endfor 133 | endif 134 | 135 | if l:jobtype ==? '' 136 | return s:job_error_unsupported_job_type 137 | endif 138 | 139 | " options shared by both vim and neovim 140 | let l:jobopt = {} 141 | if has_key(a:opts, 'cwd') 142 | let l:jobopt.cwd = a:opts.cwd 143 | endif 144 | if has_key(a:opts, 'env') 145 | let l:jobopt.env = a:opts.env 146 | endif 147 | 148 | let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw 149 | 150 | if l:jobtype == s:job_type_nvimjob 151 | if l:normalize ==# 'string' 152 | let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout_string') : function('s:noop') 153 | let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr_string') : function('s:noop') 154 | else " array or raw 155 | let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout') : function('s:noop') 156 | let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr') : function('s:noop') 157 | endif 158 | call extend(l:jobopt, { 'on_exit': function('s:on_exit') }) 159 | let l:job = jobstart(a:cmd, l:jobopt) 160 | if l:job <= 0 161 | return l:job 162 | endif 163 | let l:jobid = l:job " nvimjobid and internal jobid is same 164 | let s:jobs[l:jobid] = { 165 | \ 'type': s:job_type_nvimjob, 166 | \ 'opts': a:opts, 167 | \ } 168 | let s:jobs[l:jobid].job = l:job 169 | elseif l:jobtype == s:job_type_vimjob 170 | let s:jobidseq = s:jobidseq + 1 171 | let l:jobid = s:jobidseq 172 | if l:normalize ==# 'array' 173 | let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb_array', [l:jobid, a:opts]) : function('s:noop') 174 | let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb_array', [l:jobid, a:opts]) : function('s:noop') 175 | else " raw or string 176 | let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb', [l:jobid, a:opts]) : function('s:noop') 177 | let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb', [l:jobid, a:opts]) : function('s:noop') 178 | endif 179 | call extend(l:jobopt, { 180 | \ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]), 181 | \ 'mode': 'raw', 182 | \ }) 183 | if has('patch-8.1.889') 184 | let l:jobopt['noblock'] = 1 185 | endif 186 | let l:job = job_start(a:cmd, l:jobopt) 187 | if job_status(l:job) !=? 'run' 188 | return -1 189 | endif 190 | let s:jobs[l:jobid] = { 191 | \ 'type': s:job_type_vimjob, 192 | \ 'opts': a:opts, 193 | \ 'job': l:job, 194 | \ 'channel': job_getchannel(l:job), 195 | \ 'buffer': '' 196 | \ } 197 | else 198 | return s:job_error_unsupported_job_type 199 | endif 200 | 201 | return l:jobid 202 | endfunction 203 | 204 | function! s:job_stop(jobid) abort 205 | if has_key(s:jobs, a:jobid) 206 | let l:jobinfo = s:jobs[a:jobid] 207 | if l:jobinfo.type == s:job_type_nvimjob 208 | " See: vital-Whisky/System.Job 209 | try 210 | call jobstop(a:jobid) 211 | catch /^Vim\%((\a\+)\)\=:E900/ 212 | " NOTE: 213 | " Vim does not raise exception even the job has already closed so fail 214 | " silently for 'E900: Invalid job id' exception 215 | endtry 216 | elseif l:jobinfo.type == s:job_type_vimjob 217 | if type(s:jobs[a:jobid].job) == v:t_job 218 | call job_stop(s:jobs[a:jobid].job) 219 | elseif type(s:jobs[a:jobid].job) == v:t_channel 220 | call ch_close(s:jobs[a:jobid].job) 221 | endif 222 | endif 223 | endif 224 | endfunction 225 | 226 | function! s:job_send(jobid, data, opts) abort 227 | let l:jobinfo = s:jobs[a:jobid] 228 | let l:close_stdin = get(a:opts, 'close_stdin', 0) 229 | if l:jobinfo.type == s:job_type_nvimjob 230 | call jobsend(a:jobid, a:data) 231 | if l:close_stdin 232 | call chanclose(a:jobid, 'stdin') 233 | endif 234 | elseif l:jobinfo.type == s:job_type_vimjob 235 | " There is no easy way to know when ch_sendraw() finishes writing data 236 | " on a non-blocking channels -- has('patch-8.1.889') -- and because of 237 | " this, we cannot safely call ch_close_in(). So when we find ourselves 238 | " in this situation (i.e. noblock=1 and close stdin after send) we fall 239 | " back to using s:flush_vim_sendraw() and wait for transmit buffer to be 240 | " empty 241 | " 242 | " Ref: https://groups.google.com/d/topic/vim_dev/UNNulkqb60k/discussion 243 | if has('patch-8.1.818') && (!has('patch-8.1.889') || !l:close_stdin) 244 | call ch_sendraw(l:jobinfo.channel, a:data) 245 | else 246 | let l:jobinfo.buffer .= a:data 247 | call s:flush_vim_sendraw(a:jobid, v:null) 248 | endif 249 | if l:close_stdin 250 | while len(l:jobinfo.buffer) != 0 251 | sleep 1m 252 | endwhile 253 | call ch_close_in(l:jobinfo.channel) 254 | endif 255 | endif 256 | endfunction 257 | 258 | function! s:flush_vim_sendraw(jobid, timer) abort 259 | " https://github.com/vim/vim/issues/2548 260 | " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 261 | let l:jobinfo = s:jobs[a:jobid] 262 | sleep 1m 263 | if len(l:jobinfo.buffer) <= 4096 264 | call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer) 265 | let l:jobinfo.buffer = '' 266 | else 267 | let l:to_send = l:jobinfo.buffer[:4095] 268 | let l:jobinfo.buffer = l:jobinfo.buffer[4096:] 269 | call ch_sendraw(l:jobinfo.channel, l:to_send) 270 | call timer_start(1, function('s:flush_vim_sendraw', [a:jobid])) 271 | endif 272 | endfunction 273 | 274 | function! s:job_wait_single(jobid, timeout, start) abort 275 | if !has_key(s:jobs, a:jobid) 276 | return -3 277 | endif 278 | 279 | let l:jobinfo = s:jobs[a:jobid] 280 | if l:jobinfo.type == s:job_type_nvimjob 281 | let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000 282 | return jobwait([a:jobid], float2nr(l:timeout))[0] 283 | elseif l:jobinfo.type == s:job_type_vimjob 284 | let l:timeout = a:timeout / 1000.0 285 | try 286 | while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout 287 | let l:info = job_info(l:jobinfo.job) 288 | if l:info.status ==# 'dead' 289 | return l:info.exitval 290 | elseif l:info.status ==# 'fail' 291 | return -3 292 | endif 293 | sleep 1m 294 | endwhile 295 | catch /^Vim:Interrupt$/ 296 | return -2 297 | endtry 298 | endif 299 | return -1 300 | endfunction 301 | 302 | function! s:job_wait(jobids, timeout) abort 303 | let l:start = reltime() 304 | let l:exitcode = 0 305 | let l:ret = [] 306 | for l:jobid in a:jobids 307 | if l:exitcode != -2 " Not interrupted. 308 | let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start) 309 | endif 310 | let l:ret += [l:exitcode] 311 | endfor 312 | return l:ret 313 | endfunction 314 | 315 | function! s:job_pid(jobid) abort 316 | if !has_key(s:jobs, a:jobid) 317 | return 0 318 | endif 319 | 320 | let l:jobinfo = s:jobs[a:jobid] 321 | if l:jobinfo.type == s:job_type_nvimjob 322 | return jobpid(a:jobid) 323 | elseif l:jobinfo.type == s:job_type_vimjob 324 | let l:vimjobinfo = job_info(a:jobid) 325 | if type(l:vimjobinfo) == type({}) && has_key(l:vimjobinfo, 'process') 326 | return l:vimjobinfo['process'] 327 | endif 328 | endif 329 | return 0 330 | endfunction 331 | 332 | function! s:callback_cb(jobid, opts, ch, data) abort 333 | if has_key(a:opts, 'on_stdout') 334 | call a:opts.on_stdout(a:jobid, a:data, 'stdout') 335 | endif 336 | endfunction 337 | 338 | function! s:callback_cb_array(jobid, opts, ch, data) abort 339 | if has_key(a:opts, 'on_stdout') 340 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 341 | endif 342 | endfunction 343 | 344 | function! s:close_cb(jobid, opts, ch) abort 345 | if has_key(a:opts, 'on_exit') 346 | call a:opts.on_exit(a:jobid, 'closed', 'exit') 347 | endif 348 | if has_key(s:jobs, a:jobid) 349 | call remove(s:jobs, a:jobid) 350 | endif 351 | endfunction 352 | 353 | " public apis {{{ 354 | function! async#job#start(cmd, opts) abort 355 | return s:job_start(a:cmd, a:opts) 356 | endfunction 357 | 358 | function! async#job#stop(jobid) abort 359 | call s:job_stop(a:jobid) 360 | endfunction 361 | 362 | function! async#job#send(jobid, data, ...) abort 363 | let l:opts = get(a:000, 0, {}) 364 | call s:job_send(a:jobid, a:data, l:opts) 365 | endfunction 366 | 367 | function! async#job#wait(jobids, ...) abort 368 | let l:timeout = get(a:000, 0, -1) 369 | return s:job_wait(a:jobids, l:timeout) 370 | endfunction 371 | 372 | function! async#job#pid(jobid) abort 373 | return s:job_pid(a:jobid) 374 | endfunction 375 | 376 | function! async#job#connect(addr, opts) abort 377 | let s:jobidseq = s:jobidseq + 1 378 | let l:jobid = s:jobidseq 379 | let l:retry = 0 380 | let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw 381 | while l:retry < 5 382 | let l:ch = ch_open(a:addr, {'waittime': 1000}) 383 | call ch_setoptions(l:ch, { 384 | \ 'callback': function(l:normalize ==# 'array' ? 's:callback_cb_array' : 's:callback_cb', [l:jobid, a:opts]), 385 | \ 'close_cb': function('s:close_cb', [l:jobid, a:opts]), 386 | \ 'mode': 'raw', 387 | \}) 388 | if ch_status(l:ch) ==# 'open' 389 | break 390 | endif 391 | sleep 100m 392 | let l:retry += 1 393 | endwhile 394 | let s:jobs[l:jobid] = { 395 | \ 'type': s:job_type_vimjob, 396 | \ 'opts': a:opts, 397 | \ 'job': l:ch, 398 | \ 'channel': l:ch, 399 | \ 'buffer': '' 400 | \} 401 | return l:jobid 402 | endfunction 403 | " }}} 404 | 405 | let &cpoptions = s:save_cpo 406 | unlet s:save_cpo 407 | --------------------------------------------------------------------------------