├── .gitignore ├── README.md ├── autoload └── jqplay.vim ├── doc └── jqplay.txt └── plugin └── jqplay.vim /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | Session.vim 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-jqplay 2 | 3 | Run [jq][jq] on a json buffer, and interactively update the output window 4 | whenever the input buffer or the jq filter buffer are modified similar to 5 | [jqplay.org][jqplay]. 6 | 7 | **Requirements:** Vim 9 8 | 9 |
10 |

11 | 12 | 13 | 14 |

15 |
16 | 17 | 18 | ## Usage 19 | 20 | ### Quick Overview 21 | 22 | | Command | Description | 23 | | -------------------------------- | -------------------------------------------------------------------------------------- | 24 | | `:Jqplay [{args}]` | Start an interactive session using the current json buffer and a new jq script buffer. | 25 | | `:JqplayScratch [{args}]` | Like `:Jqplay` but creates a new scratch buffer as input. | 26 | | `:JqplayScratchNoInput [{args}]` | Like `:JqplayScratch` but doesn't pass any input file to jq. | 27 | 28 | ### `:Jqplay` 29 | 30 | Run `:Jqplay {args}` to start an interactive jq session using the current (json) 31 | buffer as input and the jq options `{args}`. The command will open two new 32 | windows: 33 | 1. The first window contains a jq scratch buffer (prefixed with `jq-filter://`) 34 | that is applied interactively to the current json buffer. 35 | 2. The second window displays the jq output (prefixed with `jq-output://`). 36 | 37 | `{args}` can be any jq command-line options as you would pass them to jq in the 38 | shell. 39 | 40 | Jq is invoked automatically whenever the input buffer or the jq filter buffer 41 | are modified. By default jq is executed when the `InsertLeave` or `TextChanged` 42 | events are triggered. See [configuration](#configuration) below for how to 43 | change the list of events when jq is invoked. 44 | 45 | Once an interactive session is started the following commands are available: 46 | * `:JqplayClose[!]` ─ Stop the interactive session. Add a `!` to also delete all 47 | associated scratch buffers. 48 | * `:Jqrun [{args}]` ─ Invoke jq manually with the jq options `{args}`. 49 | * `:Jqstop` ─ Terminate a running jq process started by this plugin. 50 | 51 | Run `:Jqrun {args}` at any time to invoke jq manually with the jq arguments 52 | `{args}` and the current `jq-filter://` buffer. This will temporarily override 53 | the jq options previously set when starting the session with `:Jqplay {args}`. 54 | Add a bang to `:Jqrun!` to permanently override the options for the 55 | `jq-filter://` buffer. 56 | 57 | `:Jqrun` is useful to quickly run the same jq filter with different set of jq 58 | options, without closing the session. Alternatively, if you don't want to run jq 59 | interactively on every buffer change, disable all autocommands and use `:Jqrun` 60 | instead. 61 | 62 | ### `:JqplayScratch` 63 | 64 | This command is like `:Jqplay` but starts an interactive jq session with a new 65 | scratch buffer as input. 66 | 67 | ### `:JqplayScratchNoInput` 68 | 69 | Opens an interactive session with a new jq filter buffer but without using any 70 | input buffer. It always passes `-n/--null-input` to jq. This command is useful 71 | when you don't need any input file passed to jq. 72 | 73 | 74 | ## Configuration 75 | 76 | Options can be set through the dictionary variable `g:jqplay`. The following 77 | entries are supported: 78 | 79 | | Key | Description | Default | 80 | | ---------- | ---------------------------------------------------------------- | -------------------------------- | 81 | | `exe` | Path to jq executable. | value found in `$PATH` | 82 | | `opts` | Default jq command-line options (e.g. `--tab`). | - | 83 | | `autocmds` | Events when jq is invoked. | `['InsertLeave', 'TextChanged']` | 84 | | `delay` | Time in ms after which jq is invoked when an event is triggered. | `500` | 85 | 86 | ### Examples 87 | 88 | 1. Use the local jq executable, and tabs for indentation. Invoke jq whenever 89 | insert mode is left, or text is changed in either insert or normal mode. 90 | ```vim 91 | g:jqplay = { 92 | exe: '~/.local/bin/jq', 93 | opts: '--tab', 94 | autocmds: ['TextChanged', 'TextChangedI', 'InsertLeave'] 95 | } 96 | ``` 97 | 2. Use tabs for indentation, do not run jq automatically on buffer change. 98 | Instead invoke jq manually with `:Jqrun`: 99 | ```vim 100 | g:jqplay = {opts: '--tab', autocmds: []} 101 | ``` 102 | 103 | 104 | ## Installation 105 | 106 | ```bash 107 | $ cd ~/.vim/pack/git-plugins/start 108 | $ git clone --depth=1 https://github.com/bfrg/vim-jqplay 109 | $ vim -u NONE -c 'helptags vim-jqplay/doc | quit' 110 | ``` 111 | **Note:** The directory name `git-plugins` is arbitrary, you can pick any other 112 | name. For more details see `:help packages`. Alternatively, use your favorite 113 | plugin manager. 114 | 115 | 116 | ## Related plugins 117 | 118 | [vim-jq][vim-jq] provides Vim runtime files like syntax highlighting for jq 119 | script files. 120 | 121 | 122 | ## License 123 | 124 | Distributed under the same terms as Vim itself. See `:help license`. 125 | 126 | [jq]: https://github.com/stedolan/jq 127 | [jqplay]: https://jqplay.org 128 | [vim-jq]: https://github.com/bfrg/vim-jq 129 | -------------------------------------------------------------------------------- /autoload/jqplay.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # ============================================================================== 3 | # Run jq interactively in Vim 4 | # File: autoload/jqplay.vim 5 | # Author: bfrg 6 | # Website: https://github.com/bfrg/vim-jqplay 7 | # Last Change: Dec 22, 2022 8 | # License: Same as Vim itself (see :h license) 9 | # ============================================================================== 10 | 11 | var is_running: bool = false # true if jqplay session running, false otherwise 12 | var in_buf: number = -1 # input buffer number (optional) 13 | var in_changedtick: number = -1 # b:changedtick of input buffer (optional) 14 | var in_timer: number = 0 # timer-ID of input buffer (optional) 15 | var filter_buf: number = 0 # filter buffer number 16 | var filter_changedtick: number = 0 # b:changedtick of filter buffer 17 | var filter_timer: number = 0 # timer-ID of filter buffer 18 | var filter_file: string = '' # full path to filter file on disk 19 | var out_buf: number = 0 # output buffer number 20 | var jq_cmd: string = '' # jq command running on buffer change 21 | var job_id: job # job object of jq process 22 | 23 | const defaults: dict = { 24 | exe: exepath('jq'), 25 | opts: '', 26 | delay: 500, 27 | autocmds: ['InsertLeave', 'TextChanged'] 28 | } 29 | 30 | def Getopt(key: string): any 31 | return get(g:, 'jqplay', {})->get(key, defaults[key]) 32 | enddef 33 | 34 | # Helper function to create full jq command 35 | def Jqcmd(exe: string, opts: string, args: string, file: string): string 36 | return $'{exe} {opts} {args} -f {file}' 37 | enddef 38 | 39 | # Is jqplay session running with input buffer? 40 | def Jq_with_input(): bool 41 | return in_buf != -1 42 | enddef 43 | 44 | def Error(msg: string) 45 | echohl ErrorMsg | echomsg '[jqplay]' msg | echohl None 46 | enddef 47 | 48 | def Warning(msg: string) 49 | echohl WarningMsg | echomsg '[jqplay]' msg | echohl None 50 | enddef 51 | 52 | def New_scratch(bufname: string, filetype: string, clean: bool, mods: string, opts: dict = {}): number 53 | const winid: number = win_getid() 54 | var bufnr: number 55 | 56 | if bufexists(bufname) 57 | bufnr = bufnr(bufname) 58 | setbufvar(bufnr, '&filetype', filetype) 59 | if clean 60 | silent deletebufline(bufnr, 1, '$') 61 | endif 62 | 63 | if bufwinnr(bufnr) > 0 64 | return bufnr 65 | else 66 | silent execute mods 'sbuffer' bufnr 67 | endif 68 | else 69 | noautocmd silent execute mods 'new' fnameescape(bufname) 70 | setlocal noswapfile buflisted buftype=nofile bufhidden=hide 71 | bufnr = bufnr() 72 | setbufvar(bufnr, '&filetype', filetype) 73 | endif 74 | 75 | if has_key(opts, 'resize') 76 | execute 'resize' opts.resize 77 | endif 78 | 79 | win_gotoid(winid) 80 | 81 | return bufnr 82 | enddef 83 | 84 | def Run_manually(bang: bool, args: string) 85 | if args =~ '\%(^\|\s\)-\a*f\>\|--from-file\>' 86 | Error('-f and --from-file options not allowed') 87 | return 88 | endif 89 | 90 | if filter_changedtick != getbufvar(filter_buf, 'changedtick') 91 | filter_buf->getbufline(1, '$')->writefile(filter_file) 92 | endif 93 | 94 | const cmd: string = Jqcmd(Getopt('exe'), Getopt('opts'), args, filter_file) 95 | Run_jq(cmd) 96 | 97 | if bang 98 | jq_cmd = cmd 99 | endif 100 | enddef 101 | 102 | def On_filter_changed() 103 | if filter_changedtick == getbufvar(filter_buf, 'changedtick') 104 | return 105 | endif 106 | 107 | filter_changedtick = getbufvar(filter_buf, 'changedtick') 108 | timer_stop(filter_timer) 109 | filter_timer = Getopt('delay')->timer_start(Filter_changed) 110 | enddef 111 | 112 | def Filter_changed(timer: number) 113 | filter_buf->getbufline(1, '$')->writefile(filter_file) 114 | Run_jq(jq_cmd) 115 | enddef 116 | 117 | def On_input_changed() 118 | if in_changedtick == getbufvar(in_buf, 'changedtick') 119 | return 120 | endif 121 | 122 | in_changedtick = getbufvar(in_buf, 'changedtick') 123 | timer_stop(in_timer) 124 | in_timer = Getopt('delay')->timer_start((_) => Run_jq(jq_cmd)) 125 | enddef 126 | 127 | def Close_cb(ch: channel) 128 | silent deletebufline(out_buf, 1) 129 | redrawstatus! 130 | enddef 131 | 132 | def Run_jq(cmd: string) 133 | silent deletebufline(out_buf, 1, '$') 134 | 135 | if exists('job_id') && job_status(job_id) == 'run' 136 | job_stop(job_id) 137 | endif 138 | 139 | final opts: dict = { 140 | in_io: 'null', 141 | out_cb: (_, msg: string) => appendbufline(out_buf, '$', msg), 142 | err_cb: (_, msg: string) => appendbufline(out_buf, '$', '// ' .. msg), 143 | close_cb: Close_cb 144 | } 145 | 146 | if Jq_with_input() 147 | extend(opts, {in_io: 'buffer', in_buf: in_buf}) 148 | endif 149 | 150 | # http//github.com/vim/vim/issues/4688 151 | try 152 | job_id = job_start([&shell, &shellcmdflag, cmd], opts) 153 | catch /^Vim\%((\a\+)\)\=:E631:/ 154 | endtry 155 | enddef 156 | 157 | def Jq_stop(arg: string = 'term') 158 | if job_status(job_id) == 'run' 159 | job_stop(job_id, arg) 160 | endif 161 | enddef 162 | 163 | def Jq_close(bang: bool) 164 | if !is_running && !(exists('#jqplay#BufDelete') || exists('#jqplay#BufWipeout')) 165 | return 166 | endif 167 | 168 | Jq_stop() 169 | autocmd_delete([{group: 'jqplay'}]) 170 | 171 | if bang 172 | execute 'bwipeout' filter_buf 173 | execute 'bwipeout' out_buf 174 | if Jq_with_input() && getbufvar(in_buf, '&buftype') == 'nofile' 175 | execute 'bwipeout' in_buf 176 | endif 177 | endif 178 | 179 | delcommand JqplayClose 180 | delcommand Jqrun 181 | delcommand Jqstop 182 | is_running = false 183 | Warning('interactive session closed') 184 | enddef 185 | 186 | # When 'in_buffer' is set to -1, no input buffer is passed to jq 187 | export def Start(mods: string, args: string, in_buffer: number) 188 | if args =~ '\%(^\|\s\)-\a*f\>\|--from-file\>' 189 | Error('-f and --from-file options not allowed') 190 | return 191 | endif 192 | 193 | if is_running 194 | Error('only one interactive session allowed') 195 | return 196 | endif 197 | 198 | is_running = true 199 | in_buf = in_buffer 200 | 201 | # Check if -r/--raw-output or -j/--join-output options are passed 202 | const out_ft: string = args =~ '\%(^\|\s\)-\a*[rj]\a*\|--\%(raw\|join\)-output\>' ? '' : 'json' 203 | 204 | # Output buffer 205 | const out_name: string = 'jq-output://' .. (in_buffer == -1 ? '' : bufname(in_buffer)) 206 | out_buf = New_scratch(out_name, out_ft, true, mods) 207 | 208 | # jq filter buffer 209 | const filter_name: string = 'jq-filter://' .. (in_buffer == -1 ? '' : bufname(in_buffer)) 210 | filter_buf = New_scratch(filter_name, 'jq', false, 'botright', {resize: 10}) 211 | 212 | # Temporary file where jq filter buffer is written to 213 | filter_file = tempname() 214 | 215 | in_changedtick = getbufvar(in_buffer, 'changedtick', -1) 216 | filter_changedtick = getbufvar(filter_buf, 'changedtick') 217 | in_timer = 0 218 | filter_timer = 0 219 | jq_cmd = Jqcmd(Getopt('exe'), Getopt('opts'), args, filter_file) 220 | 221 | command -bar -bang JqplayClose Jq_close(false) 222 | command -bar -bang -nargs=? -complete=customlist,Complete Jqrun Run_manually(false, ) 223 | command -nargs=? -complete=custom,Stopcomplete Jqstop Jq_stop() 224 | 225 | # When input, output or filter buffer are deleted/wiped out, close the 226 | # interactive session 227 | autocmd_add([ 228 | { 229 | group: 'jqplay', 230 | event: ['BufDelete', 'BufWipeout'], 231 | bufnr: out_buf, 232 | cmd: 'Jq_close(false)' 233 | }, 234 | { 235 | group: 'jqplay', 236 | event: ['BufDelete', 'BufWipeout'], 237 | bufnr: filter_buf, 238 | cmd: 'Jq_close(false)' 239 | } 240 | ]) 241 | 242 | if Jq_with_input() 243 | autocmd_add([{ 244 | group: 'jqplay', 245 | event: ['BufDelete', 'BufWipeout'], 246 | bufnr: in_buffer, 247 | cmd: 'Jq_close(false)' 248 | }]) 249 | endif 250 | 251 | # Run jq interactively when input or filter buffer are modified 252 | const events: list = Getopt('autocmds') 253 | 254 | if empty(events) 255 | return 256 | endif 257 | 258 | autocmd_add([{ 259 | group: 'jqplay', 260 | event: events, 261 | bufnr: filter_buf, 262 | cmd: 'On_filter_changed()' 263 | }]) 264 | 265 | if Jq_with_input() 266 | autocmd_add([{ 267 | group: 'jqplay', 268 | event: events, 269 | bufnr: bufnr(), 270 | cmd: 'On_input_changed()' 271 | }]) 272 | endif 273 | enddef 274 | 275 | export def Scratch(input: bool, mods: string, args: string) 276 | if is_running 277 | Error('only one interactive session allowed') 278 | return 279 | endif 280 | 281 | if args =~ '\%(^\|\s\)-\a*f\>\|--from-file\>' 282 | Error('-f and --from-file options not allowed') 283 | return 284 | endif 285 | 286 | const raw_input: bool = args =~ '\%(^\|\s\)-\a*R\a*\>\|--raw-input\>' 287 | const null_input: bool = args =~ '\%(^\|\s\)-\a*n\a*\>\|--null-input\>' 288 | 289 | if !input && raw_input && null_input 290 | Error('not possible to run :JqplayScratchNoInput with -n and -R') 291 | return 292 | endif 293 | 294 | if input 295 | tabnew 296 | setlocal buflisted buftype=nofile bufhidden=hide noswapfile 297 | if !raw_input 298 | setlocal filetype=json 299 | endif 300 | else 301 | tab split 302 | endif 303 | 304 | const arg: string = !input && !null_input ? (args .. ' -n') : args 305 | const bufnr: number = input ? bufnr() : -1 306 | Start(mods, arg, bufnr) 307 | 308 | # Close the initial window that we opened with :tab split 309 | if !input 310 | close 311 | endif 312 | enddef 313 | 314 | export def Job(): job 315 | return job_id 316 | enddef 317 | 318 | export def Stopcomplete(_, _, _): string 319 | return join(['term', 'hup', 'quit', 'int', 'kill'], "\n") 320 | enddef 321 | 322 | export def Complete(arglead: string, _, _): list 323 | if arglead[0] == '-' 324 | return copy([ 325 | '-a', '-C', '-c', '-e', '-f', '-h', '-j', '-L', '-M', 326 | '-n', '-R', '-r', '-S', '-s', '--arg', '--argfile', '--argjson', 327 | '--args', '--ascii-output', '--exit-status', '--from-file', 328 | '--color-output', '--compact-output', '--help', '--indent', 329 | '--join-output', '--jsonargs', '--monochrome-output', 330 | '--null-input', '--raw-input', '--raw-output', '--rawfile', 331 | '--run-tests', '--seq', '--slurp', '--slurpfile', '--sort-keys', 332 | '--stream', '--tab', '--unbuffered' 333 | ]) 334 | ->filter((_, i: string): bool => stridx(i, arglead) == 0) 335 | endif 336 | 337 | return arglead 338 | ->getcompletion('file') 339 | ->map((_, i: string): string => fnameescape(i)) 340 | enddef 341 | -------------------------------------------------------------------------------- /doc/jqplay.txt: -------------------------------------------------------------------------------- 1 | *jqplay.txt* Run jq (the command-line JSON processor) interactively in Vim 2 | 3 | Author: bfrg 4 | Website: https://github.com/bfrg/vim-jqplay 5 | License: Same terms as Vim itself (see |license|) 6 | 7 | ============================================================================== 8 | INTRODUCTION *jqplay* 9 | 10 | Run jq on a json buffer, and interactively 11 | update the output window whenever the input buffer or the jq filter buffer are 12 | modified, similar to https://jqplay.org. 13 | 14 | ============================================================================== 15 | USAGE *jqplay-usage* 16 | 17 | Commands ~ 18 | 19 | :Jqplay [{args}] *:Jqplay* 20 | Open a new jq scratch buffer and apply the entered jq filter to 21 | the current json buffer. The jq output is displayed in a new 22 | |:split| window, and updated interactively when both the jq 23 | scratch buffer or the input buffer are modified. 24 | 25 | {args} can be any jq command-line arguments as you would write 26 | them in the shell. Jq is always invoked with the options specified 27 | in {args}. These can be changed at any time during the session 28 | through |:Jqrun| (see below). 29 | 30 | The name of the output buffer is prefixed with "jq-output://" to 31 | distinguish it from the input buffer. 32 | 33 | The command can be preceded by a command modifier. For example, to 34 | open the output buffer in a new |:vertical| split, run: > 35 | :vertical Jqplay {args} 36 | < 37 | Possible modifiers: 38 | |:vertical| 39 | |:topleft| 40 | |:botright| 41 | |:leftabove| (same as |:aboveleft|) 42 | |:rightbelow| (same as |:belowright|) 43 | 44 | By default jq is invoked automatically when the |InsertLeave| or 45 | the |TextChanged| event is triggered. The list of autocommands can 46 | be changed with the "autocmds" entry, see |jqplay.autocmds| 47 | below. 48 | 49 | :JqplayClose[!] *:JqplayClose* 50 | Close the interactive jqplay session. This will delete all 51 | autocommands that are invoking jq on buffer change. 52 | 53 | Without the "!" all buffers are kept open. Adding "!" will also 54 | |:bwipeout| the jq filter and output buffers. If the input buffer 55 | is a scratch buffer ('buftype' is "nofile") it will be deleted as 56 | well. Think of ":JqplayClose!" as "I am done, close everything". 57 | 58 | :Jqrun[!] [{args}] *:Jqrun* 59 | Invoke jq manually with the jq command-line arguments {args}. This 60 | will temporarily override the jq options previously set with 61 | ":Jqplay {args}". Adding a [!] will permanently set the jq options 62 | of the current jq scratch buffer to {args}. 63 | 64 | This command is useful to quickly run the same jq filter with 65 | different set of jq options, without closing the session. 66 | 67 | Alternatively, if you don't like to run jq interactively on every 68 | buffer change, disable all |jqplay.autocmds| and run ":Jqrun" on 69 | demand. 70 | 71 | Note: The command is available only after running |:Jqplay| or 72 | |:JqplayScratch|. 73 | 74 | :Jqstop [{how}] *:Jqstop* 75 | Stop any running jq process that was previously started with 76 | |:Jqplay| or |:Jqrun|. When {how} is omitted, the job will be 77 | terminated. See |job_stop()| for possible {how} values. 78 | 79 | :JqplayScratch [{args}] *:JqplayScratch* 80 | Like |:Jqplay| but start an interactive session in a new tab page 81 | using a new scratch buffer as input for jq. 82 | 83 | The scratch buffer is always passed to jq as stdin, even when the 84 | -n/--null-input option has been set in {args}. 85 | 86 | :JqplayScratchNoInput [{args}] *:JqplayScratchNoInput* 87 | Like |:JqplayScratch| but creates an interactive session without 88 | any input buffer and always passes the --null-input option to jq. 89 | 90 | ============================================================================== 91 | CONFIGURATION *jqplay-config* 92 | *g:jqplay* 93 | All configuration is done through the |Dictionary| variable g:jqplay. The 94 | following entries are supported: 95 | 96 | exe *g:jqplay.exe* 97 | Path to jq executable. 98 | Default: value found in $PATH 99 | 100 | opts *g:jqplay.opts* 101 | Default options that are always passed to jq, like "--tab". The 102 | arguments {args} passed to |:Jqplay| are appended to "opts". 103 | Default: "" 104 | 105 | autocmds *g:jqplay.autocmds* 106 | List of |autocmd-events| when to invoke jq. The autocommands are 107 | set for both the input buffer and the jq scratch buffer. If you 108 | don't want to run jq interactively on every buffer change, set 109 | this entry to an empty list and run |:Jqrun| manually. 110 | Default: ["|InsertLeave|", "|TextChanged|"] 111 | 112 | delay *g:jqplay.delay* 113 | Time in ms after which jq is invoked when one of the events in 114 | |g:jqplay.autocmds| is triggered. 115 | Default: 500 116 | 117 | Examples ~ 118 | 119 | 1. Use the local jq executable and tabs for indentation. Invoke jq 120 | whenever insert mode is left, or text is changed in insert or normal 121 | mode: > 122 | 123 | g:jqplay = { 124 | exe: '~/.local/bin/jq', 125 | opts: '--tab', 126 | autocmds: ['TextChanged', 'TextChangedI', 'InsertLeave'] 127 | } 128 | < 129 | 2. Use tabs for indentation, don't run jq automatically on buffer change. 130 | Instead invoke jq manually with |:Jqrun|: > 131 | 132 | g:jqplay = {opts: '--tab', autocmds: []} 133 | < 134 | vim:tw=78:et:ft=help:norl: 135 | -------------------------------------------------------------------------------- /plugin/jqplay.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # ============================================================================== 3 | # Run jq interactively in Vim 4 | # File: plugin/jqplay.vim 5 | # Author: bfrg 6 | # Website: https://github.com/bfrg/vim-jqplay 7 | # Last Change: Dec 24, 2023 8 | # License: Same as Vim itself (see :h license) 9 | # ============================================================================== 10 | 11 | import autoload '../autoload/jqplay.vim' 12 | 13 | command -nargs=* -complete=customlist,jqplay.Complete Jqplay jqplay.Start(, , bufnr()) 14 | command -nargs=* -complete=customlist,jqplay.Complete JqplayScratch jqplay.Scratch(true, , ) 15 | command -nargs=* -complete=customlist,jqplay.Complete JqplayScratchNoInput jqplay.Scratch(false, , ) 16 | --------------------------------------------------------------------------------