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