├── .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 |
--------------------------------------------------------------------------------