added search filtering
[notmuch] / vim / plugin / notmuch.vim
1 " notmuch.vim plugin --- run notmuch within vim
2 "
3 " Copyright © Carl Worth
4 "
5 " This file is part of Notmuch.
6 "
7 " Notmuch is free software: you can redistribute it and/or modify it
8 " under the terms of the GNU General Public License as published by
9 " the Free Software Foundation, either version 3 of the License, or
10 " (at your option) any later version.
11 "
12 " Notmuch is distributed in the hope that it will be useful, but
13 " WITHOUT ANY WARRANTY; without even the implied warranty of
14 " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 " General Public License for more details.
16 "
17 " You should have received a copy of the GNU General Public License
18 " along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
19 "
20 " Authors: Bart Trojanowski <bart@jukie.net>
21
22 " --- configuration defaults {{{1
23
24 let s:notmuch_defaults = {
25         \ 'g:notmuch_cmd':                           'notmuch'                    ,
26         \ 'g:notmuch_search_reverse':                1                            ,
27         \ 'g:notmuch_show_fold_signatures':          1                            ,
28         \ 'g:notmuch_show_fold_citations':           1                            ,
29         \
30         \ 'g:notmuch_show_message_begin_regexp':     '^\fmessage{'                ,
31         \ 'g:notmuch_show_message_end_regexp':       '^\fmessage}'                ,
32         \ 'g:notmuch_show_header_begin_regexp':      '^\fheader{'                 ,
33         \ 'g:notmuch_show_header_end_regexp':        '^\fheader}'                 ,
34         \ 'g:notmuch_show_body_begin_regexp':        '^\fbody{'                   ,
35         \ 'g:notmuch_show_body_end_regexp':          '^\fbody}'                   ,
36         \ 'g:notmuch_show_attachment_begin_regexp':  '^\fattachment{'             ,
37         \ 'g:notmuch_show_attachment_end_regexp':    '^\fattachment}'             ,
38         \ 'g:notmuch_show_part_begin_regexp':        '^\fpart{'                   ,
39         \ 'g:notmuch_show_part_end_regexp':          '^\fpart}'                   ,
40         \ 'g:notmuch_show_marker_regexp':            '^\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$',
41         \
42         \ 'g:notmuch_show_message_parse_regexp':     '\(id:[^ ]*\) depth:\([0-9]*\) filename:\(.*\)$',
43         \ 'g:notmuch_show_tags_regexp':              '(\([^)]*\))$'               ,
44         \
45         \ 'g:notmuch_show_signature_regexp':         '^\(-- \?\|_\+\)$'           ,
46         \ 'g:notmuch_show_signature_lines_max':      12                           ,
47         \
48         \ 'g:notmuch_show_citation_regexp':          '^\s*>'                      ,
49         \ }
50
51 " defaults for g:notmuch_initial_search_words
52 " override with: let g:notmuch_initial_search_words = [ ... ]
53 let s:notmuch_initial_search_words_defaults = [
54         \ 'tag:inbox'
55         \ ]
56
57 " defaults for g:notmuch_show_headers
58 " override with: let g:notmuch_show_headers = [ ... ]
59 let s:notmuch_show_headers_defaults = [
60         \ 'Subject',
61         \ 'From'
62         \ ]
63
64 " --- keyboard mapping definitions {{{1
65
66 " --- --- bindings for search screen {{{2
67 let g:notmuch_search_maps = {
68         \ '<Enter>':    ':call <SID>NM_search_show_thread()<CR>',
69         \ 'a':          ':call <SID>NM_search_archive_thread()<CR>',
70         \ 'f':          ':call <SID>NM_search_filter()<CR>',
71         \ 'm':          ':call <SID>NM_new_mail()<CR>',
72         \ 'o':          ':call <SID>NM_search_toggle_order()<CR>',
73         \ 'r':          ':call <SID>NM_search_reply_to_thread()<CR>',
74         \ 's':          ':call <SID>NM_search_prompt()<CR>',
75         \ 'S':          ':call <SID>NM_search_edit()<CR>',
76         \ 't':          ':call <SID>NM_search_filter_by_tag()<CR>',
77         \ 'q':          ':call <SID>NM_kill_buffer()<CR>',
78         \ '+':          ':call <SID>NM_search_add_tags([])<CR>',
79         \ '-':          ':call <SID>NM_search_remove_tags([])<CR>',
80         \ '=':          ':call <SID>NM_search_refresh_view()<CR>',
81         \ }
82
83 " --- --- bindings for show screen {{{2
84 let g:notmuch_show_maps = {
85         \ '<C-N>':      ':call <SID>NM_cmd_show_next()<CR>',
86         \ 'c':          ':call <SID>NM_cmd_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)<CR>',
87         \ 's':          ':call <SID>NM_cmd_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)<CR>',
88         \ 'q':          ':call <SID>NM_kill_buffer()<CR>',
89         \ }
90
91 " --- implement search screen {{{1
92
93 function! s:NM_cmd_search(words)
94         let cmd = ['search']
95         if g:notmuch_search_reverse
96                 let cmd = cmd + ['--reverse']
97         endif
98         let data = s:NM_run(cmd + a:words)
99         "let data = substitute(data, '27/27', '25/27', '')
100         "let data = substitute(data, '\[4/4\]', '[0/4]', '')
101         let lines = split(data, "\n")
102         let disp = copy(lines)
103         call map(disp, 'substitute(v:val, "^thread:\\S* ", "", "")' )
104
105         call <SID>NM_newBuffer('search', join(disp, "\n"))
106         let b:nm_raw_lines = lines
107         let b:nm_search_words = a:words
108
109         call <SID>NM_set_map(g:notmuch_search_maps)
110         setlocal cursorline
111         setlocal nowrap
112 endfunction
113
114 " --- --- search screen action functions {{{2
115
116 function! s:NM_search_show_thread()
117         let id = <SID>NM_search_find_thread_id()
118         if id != ''
119                 call <SID>NM_cmd_show([id])
120         endif
121 endfunction
122
123 function! s:NM_search_prompt()
124         " TODO: input() can support completion
125         let text = input('NotMuch Search: ')
126         if strlen(text)
127                 let tags = split(text)
128         else
129                 let tags = s:notmuch_initial_search_words_defaults
130         endif
131         setlocal bufhidden=delete
132         call <SID>NM_cmd_search(tags)
133 endfunction
134
135 function! s:NM_search_edit()
136         " TODO: input() can support completion
137         let text = input('NotMuch Search: ', join(b:nm_search_words, ' '))
138         if strlen(text)
139                 call <SID>NM_cmd_search(split(text))
140         endif
141 endfunction
142
143 function! s:NM_search_archive_thread()
144         call <SID>NM_add_remove_tags('-', ['inbox'])
145         " TODO: this could be made better and more generic
146         setlocal modifiable
147         s/(\([^)]*\)\<inbox\>\([^)]*\))$/(\1\2)/
148         setlocal nomodifiable
149         norm j
150 endfunction
151
152 function! s:NM_search_filter()
153         call <SID>NM_search_filter_helper('Filter: ', '')
154 endfunction
155
156 function! s:NM_search_filter_by_tag()
157         call <SID>NM_search_filter_helper('Filter Tag(s): ', 'tag:')
158 endfunction
159
160 function! s:NM_search_filter_helper(prompt, prefix)
161         " TODO: input() can support completion
162         let text = input(a:prompt)
163         if !strlen(text)
164                 return
165         endif
166
167         let tags = split(text)
168         map(tags, 'a:prefix . v:val')
169         let tags = b:nm_search_words + tags
170         echo tags
171
172         let prev_bufnr = bufnr('%')
173         setlocal bufhidden=hide
174         call <SID>NM_cmd_search(tags)
175         setlocal bufhidden=delete
176         let b:nm_prev_bufnr = prev_bufnr
177 endfunction
178
179 function! s:NM_new_mail()
180         echoe 'Not implemented'
181 endfunction
182
183 function! s:NM_search_toggle_order()
184         echoe 'Not implemented'
185 endfunction
186
187 function! s:NM_search_reply_to_thread()
188         echoe 'Not implemented'
189 endfunction
190
191 function! s:NM_search_add_tags(tags)
192         call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
193 endfunction
194
195 function! s:NM_search_remove_tags(tags)
196         call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
197 endfunction
198
199 function! s:NM_search_refresh_view()
200         let lno = line('.')
201         call <SID>NM_cmd_search(b:nm_search_words)
202         " FIXME: should find the line of the thread we were on if possible
203         exec printf('norm %dG', lno)
204 endfunction
205
206 " --- --- search screen helper functions {{{2
207
208 function! s:NM_search_find_thread_id()
209         if !exists('b:nm_raw_lines')
210                 echoe 'no b:nm_raw_lines'
211                 return ''
212         else
213                 let line = line('.')
214                 let info = b:nm_raw_lines[line-1]
215                 let what = split(info, '\s\+')[0]
216                 return what
217         endif
218 endfunction
219
220 function! s:NM_search_add_remove_tags(prompt, prefix, intags)
221         if type(a:intags) != type([]) || len(a:intags) == 0
222                 " TODO: input() can support completion
223                 let text = input(a:prompt)
224                 if !strlen(text)
225                         return
226                 endif
227                 call <SID>NM_add_remove_tags(prefix, split(text, ' '))
228         else
229                 call <SID>NM_add_remove_tags(prefix, a:intags)
230         endif
231         call <SID>NM_search_refresh_view()
232 endfunction
233
234 function! s:NM_add_remove_tags(prefix, tags)
235         let id = <SID>NM_search_find_thread_id()
236         if id == ''
237                 echoe 'Eeek! I couldn''t find the thead id!'
238         endif
239         call map(a:tags, 'a:prefix . v:val')
240         " TODO: handle errors
241         call <SID>NM_run(['tag'] + a:tags + ['--', id])
242 endfunction
243
244 " --- implement show screen {{{1
245
246 function! s:NM_cmd_show(words)
247         let prev_bufnr = bufnr('%')
248         let data = s:NM_run(['show'] + a:words)
249         let lines = split(data, "\n")
250
251         let info = s:NM_cmd_show_parse(lines)
252
253         setlocal bufhidden=hide
254         call <SID>NM_newBuffer('show', join(info['disp'], "\n"))
255         setlocal bufhidden=delete
256         let b:nm_raw_info = info
257         let b:nm_prev_bufnr = prev_bufnr
258
259         call <SID>NM_cmd_show_mkfolds()
260         call <SID>NM_cmd_show_mksyntax()
261         call <SID>NM_set_map(g:notmuch_show_maps)
262         setlocal foldtext=NM_cmd_show_foldtext()
263         setlocal fillchars=
264         setlocal foldcolumn=6
265
266 endfunction
267
268 function! s:NM_kill_buffer()
269         if exists('b:nm_prev_bufnr')
270                 setlocal bufhidden=delete
271                 exec printf(":buffer %d", b:nm_prev_bufnr)
272         else
273                 echo "Nothing to kill."
274         endif
275 endfunction
276
277 function! s:NM_cmd_show_next()
278         let info = b:nm_raw_info
279         let lnum = line('.')
280         let cnt = 0
281         for msg in info['msgs']
282                 let cnt = cnt + 1
283                 if lnum >= msg['start']
284                         continue
285                 endif
286
287                 exec printf('norm %dG', msg['start'])
288                 norm zz
289                 return
290         endfor
291         norm qj
292         call <SID>NM_search_show_thread()
293 endfunction
294
295 function! s:NM_cmd_show_fold_toggle(key, type, fold)
296         let info = b:nm_raw_info
297         let act = 'open'
298         if a:fold
299                 let act = 'close'
300         endif
301         for fld in info['folds']
302                 if fld[0] == a:type
303                         exec printf('%dfold%s', fld[1], act)
304                 endif
305         endfor
306         exec printf('nnoremap <buffer> %s :call <SID>NM_cmd_show_fold_toggle(''%s'', ''%s'', %d)<CR>', a:key, a:key, a:type, !a:fold)
307 endfunction
308
309
310 " s:NM_cmd_show_parse returns the following dictionary:
311 "    'disp':     lines to display
312 "    'msgs':     message info dicts { start, end, id, depth, filename, descr, header }
313 "    'folds':    fold info arrays [ type, start, end ]
314 "    'foldtext': fold text indexed by start line
315 function! s:NM_cmd_show_parse(inlines)
316         let info = { 'disp': [],       
317                    \ 'msgs': [],       
318                    \ 'folds': [],      
319                    \ 'foldtext': {} }  
320         let msg = {}
321         let hdr = {}
322
323         let in_message = 0
324         let in_header = 0
325         let in_body = 0
326         let in_part = ''
327
328         let body_start = -1
329         let part_start = -1
330
331         let mode_type = ''
332         let mode_start = -1
333
334         let inlnum = 0
335         for line in a:inlines
336                 let inlnum = inlnum + 1
337                 let foldinfo = []
338
339                 if strlen(in_part)
340                         let part_end = 0
341
342                         if match(line, g:notmuch_show_part_end_regexp) != -1
343                                 let part_end = len(info['disp'])
344                         else
345                                 call add(info['disp'], line)
346                         endif
347
348                         if in_part == 'text/plain'
349                                 if !part_end && mode_type == ''
350                                         if match(line, g:notmuch_show_signature_regexp) != -1
351                                                 let mode_type = 'sig'
352                                                 let mode_start = len(info['disp'])
353                                         elseif match(line, g:notmuch_show_citation_regexp) != -1
354                                                 let mode_type = 'cit'
355                                                 let mode_start = len(info['disp'])
356                                         endif
357                                 elseif mode_type == 'cit'
358                                         if part_end || match(line, g:notmuch_show_citation_regexp) == -1
359                                                 let outlnum = len(info['disp'])
360                                                 let foldinfo = [ mode_type, mode_start, outlnum,
361                                                                \ printf('[ %d-line citation.  Press "c" to show. ]', outlnum - mode_start) ]
362                                                 let mode_type = ''
363                                         endif
364                                 elseif mode_type == 'sig'
365                                         let outlnum = len(info['disp'])
366                                         if (outlnum - mode_start) > g:notmuch_show_signature_lines_max
367                                                 echoe 'line ' . outlnum . ' stopped matching'
368                                                 let mode_type = ''
369                                         elseif part_end
370                                                 let foldinfo = [ mode_type, mode_start, outlnum,
371                                                                \ printf('[ %d-line signature.  Press "s" to show. ]', outlnum - mode_start) ]
372                                                 let mode_type = ''
373                                         endif
374                                 endif
375                         endif
376
377                         if part_end
378                                 " FIXME: this is a hack for handling two folds being added for one line
379                                 "         we should handle addinga fold in a function
380                                 if len(foldinfo)
381                                         call add(info['folds'], foldinfo[0:2])
382                                         let info['foldtext'][foldinfo[1]] = foldinfo[3]
383                                 endif
384
385                                 let foldinfo = [ 'text', part_start, part_end,
386                                                \ printf('[ %d-line %s.  Press "p" to show. ]', part_end - part_start, in_part) ]
387                                 let in_part = ''
388                                 call add(info['disp'], '')
389                         endif
390
391                 elseif in_body
392                         if !has_key(msg,'body_start')
393                                 let msg['body_start'] = len(info['disp']) + 1
394                         endif
395                         if match(line, g:notmuch_show_body_end_regexp) != -1
396                                 let body_end = len(info['disp'])
397                                 let foldinfo = [ 'body', body_start, body_end,
398                                                \ printf('[ BODY %d - %d lines ]', len(info['msgs']), body_end - body_start) ]
399
400                                 let in_body = 0
401
402                         elseif match(line, g:notmuch_show_part_begin_regexp) != -1
403                                 let m = matchlist(line, 'ID: \(\d\+\), Content-type: \(\S\+\)')
404                                 let in_part = 'unknown'
405                                 if len(m)
406                                         let in_part = m[2]
407                                 endif
408                                 call add(info['disp'],
409                                          \ printf('--- %s ---', in_part))
410                                 let part_start = len(info['disp']) + 1
411                         endif
412
413                 elseif in_header
414                         if in_header == 1
415                                 let msg['descr'] = line
416                                 call add(info['disp'], line)
417                                 let in_header = 2
418                                 let msg['hdr_start'] = len(info['disp']) + 1
419
420                         else
421                                 if match(line, g:notmuch_show_header_end_regexp) != -1
422                                         let msg['header'] = hdr
423                                         let in_header = 0
424                                         let hdr = {}
425                                 else
426                                         let m = matchlist(line, '^\(\w\+\):\s*\(.*\)$')
427                                         if len(m)
428                                                 let hdr[m[1]] = m[2]
429                                                 if match(g:notmuch_show_headers, m[1]) != -1
430                                                         call add(info['disp'], line)
431                                                 endif
432                                         endif
433                                 endif
434                         endif
435
436                 elseif in_message
437                         if match(line, g:notmuch_show_message_end_regexp) != -1
438                                 let msg['end'] = len(info['disp'])
439                                 call add(info['disp'], '')
440
441                                 let foldinfo = [ 'match', msg['start'], msg['end'],
442                                                \ printf('[ MSG %d - %s ]', len(info['msgs']), msg['descr']) ]
443
444                                 call add(info['msgs'], msg)
445                                 let msg = {}
446                                 let in_message = 0
447                                 let in_header = 0
448                                 let in_body = 0
449                                 let in_part = ''
450
451                         elseif match(line, g:notmuch_show_header_begin_regexp) != -1
452                                 let in_header = 1
453                                 continue
454
455                         elseif match(line, g:notmuch_show_body_begin_regexp) != -1
456                                 let body_start = len(info['disp']) + 1
457                                 let in_body = 1
458                                 continue
459                         endif
460
461                 else
462                         if match(line, g:notmuch_show_message_begin_regexp) != -1
463                                 let msg['start'] = len(info['disp']) + 1
464
465                                 let m = matchlist(line, g:notmuch_show_message_parse_regexp)
466                                 if len(m)
467                                         let msg['id'] = m[1]
468                                         let msg['depth'] = m[2]
469                                         let msg['filename'] = m[3]
470                                 endif
471
472                                 let in_message = 1
473                         endif
474                 endif
475
476                 if len(foldinfo)
477                         call add(info['folds'], foldinfo[0:2])
478                         let info['foldtext'][foldinfo[1]] = foldinfo[3]
479                 endif
480         endfor
481         return info
482 endfunction
483
484 function! s:NM_cmd_show_mkfolds()
485         let info = b:nm_raw_info
486
487         for afold in info['folds']
488                 exec printf('%d,%dfold', afold[1], afold[2])
489                 if (afold[0] == 'sig' && g:notmuch_show_fold_signatures)
490                  \ || (afold[0] == 'cit' && g:notmuch_show_fold_citations)
491                         exec printf('%dfoldclose', afold[1])
492                 else
493                         exec printf('%dfoldopen', afold[1])
494                 endif
495         endfor
496 endfunction
497
498 function! s:NM_cmd_show_mksyntax()
499         let info = b:nm_raw_info
500         let cnt = 0
501         for msg in info['msgs']
502                 let cnt = cnt + 1
503                 let start = msg['start']
504                 let hdr_start = msg['hdr_start']
505                 let body_start = msg['body_start']
506                 let end = msg['end']
507                 exec printf('syntax region nmShowMsg%dDesc start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgDesc', cnt, start, start+1)
508                 exec printf('syntax region nmShowMsg%dHead start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgHead', cnt, hdr_start, body_start)
509                 exec printf('syntax region nmShowMsg%dBody start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgBody', cnt, body_start, end)
510         endfor
511 endfunction
512
513 function! NM_cmd_show_foldtext()
514         let foldtext = b:nm_raw_info['foldtext']
515         return foldtext[v:foldstart]
516 endfunction
517
518
519 " --- notmuch helper functions {{{1
520
521 function! s:NM_newBuffer(ft, content)
522         enew
523         setlocal buftype=nofile readonly modifiable
524         silent put=a:content
525         keepjumps 0d
526         setlocal nomodifiable
527         execute printf('set filetype=notmuch-%s', a:ft)
528         execute printf('set syntax=notmuch-%s', a:ft)
529 endfunction
530
531 function! s:NM_run(args)
532         let cmd = g:notmuch_cmd . ' ' . join(a:args) . '< /dev/null'
533         let out = system(cmd)
534         if v:shell_error
535                 echohl Error
536                 echo substitute(out, '\n*$', '', '')
537                 echohl None
538                 return ''
539         else
540                 return out
541         endif
542 endfunction
543
544 " --- process and set the defaults {{{1
545
546 function! NM_set_defaults(force)
547         for [key, dflt] in items(s:notmuch_defaults)
548                 let cmd = ''
549                 if !a:force && exists(key) && type(dflt) == type(eval(key))
550                         continue
551                 elseif type(dflt) == type(0)
552                         let cmd = printf('let %s = %d', key, dflt)
553                 elseif type(dflt) == type('')
554                         let cmd = printf('let %s = ''%s''', key, dflt)
555                 "elseif type(dflt) == type([])
556                 "        let cmd = printf('let %s = %s', key, string(dflt))
557                 else
558                         echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]',
559                                                 \ a:force, key, string(dflt))
560                         continue
561                 endif
562                 exec cmd
563         endfor
564 endfunction
565 call NM_set_defaults(0)
566
567 " for some reason NM_set_defaults() didn't work for arrays...
568 if !exists('g:notmuch_show_headers')
569         let g:notmuch_show_headers = s:notmuch_show_headers_defaults
570 endif
571 if !exists('g:notmuch_initial_search_words')
572         let g:notmuch_initial_search_words = s:notmuch_initial_search_words_defaults
573 endif
574
575
576 " --- assign keymaps {{{1
577
578 function! s:NM_set_map(maps)
579         for [key, code] in items(a:maps)
580                 exec printf('nnoremap <buffer> %s %s', key, code)
581         endfor
582 endfunction
583
584 " --- command handler {{{1
585
586 function! NotMuch(args)
587         if !strlen(a:args)
588                 if exists('b:nm_search_words')
589                         let words = b:nm_search_words
590                 else
591                         let words = g:notmuch_initial_search_words
592                 endif
593                 call <SID>NM_cmd_search(words)
594                 return
595         endif
596
597         echo "blarg!"
598
599         let words = split(a:args)
600         " TODO: handle commands passed as arguments
601 endfunction
602 function! CompleteNotMuch(arg_lead, cmd_line, cursor_pos)
603         return []
604 endfunction
605
606
607 " --- glue {{{1
608
609 command! -nargs=* -complete=customlist,CompleteNotMuch NotMuch call NotMuch(<q-args>)
610 cabbrev  notmuch <c-r>=(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch')<CR>
611
612 " --- hacks, only for development :) {{{1
613
614 nnoremap ,nmr :source ~/.vim/plugin/notmuch.vim<CR>:call NotMuch('')<CR>
615
616 " vim: set ft=vim ts=8 sw=8 et foldmethod=marker :