1 " notmuch.vim plugin --- run notmuch within vim
3 " Copyright © Carl Worth
5 " This file is part of Notmuch.
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.
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.
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/>.
20 " Authors: Bart Trojanowski <bart@jukie.net>
22 " --- configuration defaults {{{1
24 let s:notmuch_defaults = {
25 \ 'g:notmuch_cmd': 'notmuch' ,
27 \ 'g:notmuch_search_newest_first': 1 ,
28 \ 'g:notmuch_search_from_column_width': 20 ,
30 \ 'g:notmuch_show_fold_signatures': 1 ,
31 \ 'g:notmuch_show_fold_citations': 1 ,
33 \ 'g:notmuch_show_message_begin_regexp': '^
\fmessage{' ,
34 \ 'g:notmuch_show_message_end_regexp': '^
\fmessage}' ,
35 \ 'g:notmuch_show_header_begin_regexp': '^
\fheader{' ,
36 \ 'g:notmuch_show_header_end_regexp': '^
\fheader}' ,
37 \ 'g:notmuch_show_body_begin_regexp': '^
\fbody{' ,
38 \ 'g:notmuch_show_body_end_regexp': '^
\fbody}' ,
39 \ 'g:notmuch_show_attachment_begin_regexp': '^
\fattachment{' ,
40 \ 'g:notmuch_show_attachment_end_regexp': '^
\fattachment}' ,
41 \ 'g:notmuch_show_part_begin_regexp': '^
\fpart{' ,
42 \ 'g:notmuch_show_part_end_regexp': '^
\fpart}' ,
43 \ 'g:notmuch_show_marker_regexp': '^
\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$',
45 \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) filename:\(.*\)$',
46 \ 'g:notmuch_show_tags_regexp': '(\([^)]*\))$' ,
48 \ 'g:notmuch_show_signature_regexp': '^\(-- \?\|_\+\)$' ,
49 \ 'g:notmuch_show_signature_lines_max': 12 ,
51 \ 'g:notmuch_show_citation_regexp': '^\s*>' ,
54 " defaults for g:notmuch_initial_search_words
55 " override with: let g:notmuch_initial_search_words = [ ... ]
56 let s:notmuch_initial_search_words_defaults = [
60 " defaults for g:notmuch_show_headers
61 " override with: let g:notmuch_show_headers = [ ... ]
62 let s:notmuch_show_headers_defaults = [
67 " --- keyboard mapping definitions {{{1
69 " --- --- bindings for search screen {{{2
70 let g:notmuch_search_maps = {
71 \ '<Enter>': ':call <SID>NM_search_show_thread()<CR>',
72 \ 'a': ':call <SID>NM_search_archive_thread()<CR>',
73 \ 'f': ':call <SID>NM_search_filter()<CR>',
74 \ 'm': ':call <SID>NM_new_mail()<CR>',
75 \ 'o': ':call <SID>NM_search_toggle_order()<CR>',
76 \ 'r': ':call <SID>NM_search_reply_to_thread()<CR>',
77 \ 's': ':call <SID>NM_search_prompt()<CR>',
78 \ 'S': ':call <SID>NM_search_edit()<CR>',
79 \ 't': ':call <SID>NM_search_filter_by_tag()<CR>',
80 \ 'q': ':call <SID>NM_kill_buffer()<CR>',
81 \ '+': ':call <SID>NM_search_add_tags([])<CR>',
82 \ '-': ':call <SID>NM_search_remove_tags([])<CR>',
83 \ '=': ':call <SID>NM_search_refresh_view()<CR>',
86 " --- --- bindings for show screen {{{2
87 let g:notmuch_show_maps = {
88 \ '<C-N>': ':call <SID>NM_cmd_show_next()<CR>',
89 \ 'c': ':call <SID>NM_cmd_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)<CR>',
90 \ 's': ':call <SID>NM_cmd_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)<CR>',
91 \ 'q': ':call <SID>NM_kill_buffer()<CR>',
94 " --- implement search screen {{{1
96 function! s:NM_cmd_search(words)
98 if g:notmuch_search_newest_first
99 let cmd = cmd + ['--sort=newest-first']
101 let cmd = cmd + ['--sort=oldest-first']
103 let data = s:NM_run(cmd + a:words)
104 "let data = substitute(data, '27/27', '25/27', '')
105 "let data = substitute(data, '\[4/4\]', '[0/4]', '')
106 let lines = split(data, "\n")
107 let disp = copy(lines)
108 "call map(disp, 'substitute(v:val, "^thread:\\S* ", "", "")' )
109 call map(disp, 's:NM_cmd_search_fmtline(v:val)')
111 call <SID>NM_newBuffer('search', join(disp, "\n"))
112 let b:nm_raw_lines = lines
113 let b:nm_search_words = a:words
115 call <SID>NM_cmd_search_mksyntax()
116 call <SID>NM_set_map(g:notmuch_search_maps)
120 function! s:NM_cmd_search_fmtline(line)
121 let m = matchlist(a:line, '^\(thread:\S\+\)\s\([^]]\+\]\) \([^;]\+\); \(.*\) (\([^(]*\))$')
123 return 'ERROR PARSING: ' . a:line
125 let max = g:notmuch_search_from_column_width
127 if strlen(from) >= max
128 let from = m[3][0:max-4] . '...'
130 return printf('%s %-20s | %s (%s)', m[2], from, m[4], m[5])
132 function! s:NM_cmd_search_mksyntax()
133 syntax clear nmSearchFrom
134 "syntax region nmSearchFrom start='\]\@<=' end='.'me=e+5,he=e+5,re=e+5 oneline contained
135 "syntax match nmSearchFrom /\]\@<=.\{10\}/ oneline contained
136 exec printf('syntax match nmSearchFrom /\(\] \)\@<=.\{%d\}/ oneline contained', g:notmuch_search_from_column_width)
137 "exec printf('syntax region nmSearchFrom start=''\%%%dv'' end=''\%%%dv'' oneline contained', 20, 30)
140 " --- --- search screen action functions {{{2
142 function! s:NM_search_show_thread()
143 let id = <SID>NM_search_find_thread_id()
145 call <SID>NM_cmd_show([id])
149 function! s:NM_search_prompt()
150 " TODO: input() can support completion
151 let text = input('NotMuch Search: ')
153 let tags = split(text)
155 let tags = s:notmuch_initial_search_words_defaults
157 setlocal bufhidden=delete
158 call <SID>NM_cmd_search(tags)
161 function! s:NM_search_edit()
162 " TODO: input() can support completion
163 let text = input('NotMuch Search: ', join(b:nm_search_words, ' '))
165 call <SID>NM_cmd_search(split(text))
169 function! s:NM_search_archive_thread()
170 call <SID>NM_add_remove_tags('-', ['inbox'])
171 " TODO: this could be made better and more generic
173 s/(\([^)]*\)\<inbox\>\([^)]*\))$/(\1\2)/
174 setlocal nomodifiable
178 function! s:NM_search_filter()
179 call <SID>NM_search_filter_helper('Filter: ', '')
182 function! s:NM_search_filter_by_tag()
183 call <SID>NM_search_filter_helper('Filter Tag(s): ', 'tag:')
186 function! s:NM_search_filter_helper(prompt, prefix)
187 " TODO: input() can support completion
188 let text = input(a:prompt)
193 let tags = split(text)
194 map(tags, 'and a:prefix . v:val')
195 let tags = b:nm_search_words + tags
198 let prev_bufnr = bufnr('%')
199 setlocal bufhidden=hide
200 call <SID>NM_cmd_search(tags)
201 setlocal bufhidden=delete
202 let b:nm_prev_bufnr = prev_bufnr
205 function! s:NM_new_mail()
206 echoe 'Not implemented'
209 function! s:NM_search_toggle_order()
210 let g:notmuch_search_newest_first = !g:notmuch_search_newest_first
211 " FIXME: maybe this would be better done w/o reading re-reading the lines
212 " reversing the b:nm_raw_lines and the buffer lines would be better
213 call <SID>NM_search_refresh_view()
216 function! s:NM_search_reply_to_thread()
217 echoe 'Not implemented'
220 function! s:NM_search_add_tags(tags)
221 call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
224 function! s:NM_search_remove_tags(tags)
225 call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
228 function! s:NM_search_refresh_view()
230 setlocal bufhidden=delete
231 call <SID>NM_cmd_search(b:nm_search_words)
232 " FIXME: should find the line of the thread we were on if possible
233 exec printf('norm %dG', lno)
236 " --- --- search screen helper functions {{{2
238 function! s:NM_search_find_thread_id()
239 if !exists('b:nm_raw_lines')
240 echoe 'no b:nm_raw_lines'
244 let info = b:nm_raw_lines[line-1]
245 let what = split(info, '\s\+')[0]
250 function! s:NM_search_add_remove_tags(prompt, prefix, intags)
251 if type(a:intags) != type([]) || len(a:intags) == 0
252 " TODO: input() can support completion
253 let text = input(a:prompt)
257 call <SID>NM_add_remove_tags(a:prefix, split(text, ' '))
259 call <SID>NM_add_remove_tags(a:prefix, a:intags)
261 call <SID>NM_search_refresh_view()
264 function! s:NM_add_remove_tags(prefix, tags)
265 let id = <SID>NM_search_find_thread_id()
267 echoe 'Eeek! I couldn''t find the thead id!'
269 call map(a:tags, 'a:prefix . v:val')
270 " TODO: handle errors
271 call <SID>NM_run(['tag'] + a:tags + ['--', id])
274 " --- implement show screen {{{1
276 function! s:NM_cmd_show(words)
277 let prev_bufnr = bufnr('%')
278 let data = s:NM_run(['show'] + a:words)
279 let lines = split(data, "\n")
281 let info = s:NM_cmd_show_parse(lines)
283 setlocal bufhidden=hide
284 call <SID>NM_newBuffer('show', join(info['disp'], "\n"))
285 setlocal bufhidden=delete
286 let b:nm_raw_info = info
287 let b:nm_prev_bufnr = prev_bufnr
289 call <SID>NM_cmd_show_mkfolds()
290 call <SID>NM_cmd_show_mksyntax()
291 call <SID>NM_set_map(g:notmuch_show_maps)
292 setlocal foldtext=NM_cmd_show_foldtext()
294 setlocal foldcolumn=6
298 function! s:NM_kill_buffer()
299 if exists('b:nm_prev_bufnr')
300 setlocal bufhidden=delete
301 exec printf(":buffer %d", b:nm_prev_bufnr)
303 echo "Nothing to kill."
307 function! s:NM_cmd_show_next()
308 let info = b:nm_raw_info
311 for msg in info['msgs']
313 if lnum >= msg['start']
317 exec printf('norm %dG', msg['start'])
322 call <SID>NM_search_show_thread()
325 function! s:NM_cmd_show_fold_toggle(key, type, fold)
326 let info = b:nm_raw_info
331 for fld in info['folds']
333 exec printf('%dfold%s', fld[1], act)
336 exec printf('nnoremap <buffer> %s :call <SID>NM_cmd_show_fold_toggle(''%s'', ''%s'', %d)<CR>', a:key, a:key, a:type, !a:fold)
340 " s:NM_cmd_show_parse returns the following dictionary:
341 " 'disp': lines to display
342 " 'msgs': message info dicts { start, end, id, depth, filename, descr, header }
343 " 'folds': fold info arrays [ type, start, end ]
344 " 'foldtext': fold text indexed by start line
345 function! s:NM_cmd_show_parse(inlines)
346 let info = { 'disp': [],
365 for line in a:inlines
366 let inlnum = inlnum + 1
372 if match(line, g:notmuch_show_part_end_regexp) != -1
373 let part_end = len(info['disp'])
375 call add(info['disp'], line)
378 if in_part == 'text/plain'
379 if !part_end && mode_type == ''
380 if match(line, g:notmuch_show_signature_regexp) != -1
381 let mode_type = 'sig'
382 let mode_start = len(info['disp'])
383 elseif match(line, g:notmuch_show_citation_regexp) != -1
384 let mode_type = 'cit'
385 let mode_start = len(info['disp'])
387 elseif mode_type == 'cit'
388 if part_end || match(line, g:notmuch_show_citation_regexp) == -1
389 let outlnum = len(info['disp'])
390 let foldinfo = [ mode_type, mode_start, outlnum,
391 \ printf('[ %d-line citation. Press "c" to show. ]', outlnum - mode_start) ]
394 elseif mode_type == 'sig'
395 let outlnum = len(info['disp'])
396 if (outlnum - mode_start) > g:notmuch_show_signature_lines_max
397 echoe 'line ' . outlnum . ' stopped matching'
400 let foldinfo = [ mode_type, mode_start, outlnum,
401 \ printf('[ %d-line signature. Press "s" to show. ]', outlnum - mode_start) ]
408 " FIXME: this is a hack for handling two folds being added for one line
409 " we should handle addinga fold in a function
411 call add(info['folds'], foldinfo[0:2])
412 let info['foldtext'][foldinfo[1]] = foldinfo[3]
415 let foldinfo = [ 'text', part_start, part_end,
416 \ printf('[ %d-line %s. Press "p" to show. ]', part_end - part_start, in_part) ]
418 call add(info['disp'], '')
422 if !has_key(msg,'body_start')
423 let msg['body_start'] = len(info['disp']) + 1
425 if match(line, g:notmuch_show_body_end_regexp) != -1
426 let body_end = len(info['disp'])
427 let foldinfo = [ 'body', body_start, body_end,
428 \ printf('[ BODY %d - %d lines ]', len(info['msgs']), body_end - body_start) ]
432 elseif match(line, g:notmuch_show_part_begin_regexp) != -1
433 let m = matchlist(line, 'ID: \(\d\+\), Content-type: \(\S\+\)')
434 let in_part = 'unknown'
438 call add(info['disp'],
439 \ printf('--- %s ---', in_part))
440 let part_start = len(info['disp']) + 1
445 let msg['descr'] = line
446 call add(info['disp'], line)
448 let msg['hdr_start'] = len(info['disp']) + 1
451 if match(line, g:notmuch_show_header_end_regexp) != -1
452 let msg['header'] = hdr
456 let m = matchlist(line, '^\(\w\+\):\s*\(.*\)$')
459 if match(g:notmuch_show_headers, m[1]) != -1
460 call add(info['disp'], line)
467 if match(line, g:notmuch_show_message_end_regexp) != -1
468 let msg['end'] = len(info['disp'])
469 call add(info['disp'], '')
471 let foldinfo = [ 'match', msg['start'], msg['end'],
472 \ printf('[ MSG %d - %s ]', len(info['msgs']), msg['descr']) ]
474 call add(info['msgs'], msg)
481 elseif match(line, g:notmuch_show_header_begin_regexp) != -1
485 elseif match(line, g:notmuch_show_body_begin_regexp) != -1
486 let body_start = len(info['disp']) + 1
492 if match(line, g:notmuch_show_message_begin_regexp) != -1
493 let msg['start'] = len(info['disp']) + 1
495 let m = matchlist(line, g:notmuch_show_message_parse_regexp)
498 let msg['depth'] = m[2]
499 let msg['filename'] = m[3]
507 call add(info['folds'], foldinfo[0:2])
508 let info['foldtext'][foldinfo[1]] = foldinfo[3]
514 function! s:NM_cmd_show_mkfolds()
515 let info = b:nm_raw_info
517 for afold in info['folds']
518 exec printf('%d,%dfold', afold[1], afold[2])
519 if (afold[0] == 'sig' && g:notmuch_show_fold_signatures)
520 \ || (afold[0] == 'cit' && g:notmuch_show_fold_citations)
521 exec printf('%dfoldclose', afold[1])
523 exec printf('%dfoldopen', afold[1])
528 function! s:NM_cmd_show_mksyntax()
529 let info = b:nm_raw_info
531 for msg in info['msgs']
533 let start = msg['start']
534 let hdr_start = msg['hdr_start']
535 let body_start = msg['body_start']
537 exec printf('syntax region nmShowMsg%dDesc start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgDesc', cnt, start, start+1)
538 exec printf('syntax region nmShowMsg%dHead start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgHead', cnt, hdr_start, body_start)
539 exec printf('syntax region nmShowMsg%dBody start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgBody', cnt, body_start, end)
543 function! NM_cmd_show_foldtext()
544 let foldtext = b:nm_raw_info['foldtext']
545 return foldtext[v:foldstart]
549 " --- notmuch helper functions {{{1
551 function! s:NM_newBuffer(ft, content)
553 setlocal buftype=nofile readonly modifiable
556 setlocal nomodifiable
557 execute printf('set filetype=notmuch-%s', a:ft)
558 execute printf('set syntax=notmuch-%s', a:ft)
561 function! s:NM_run(args)
562 let cmd = g:notmuch_cmd . ' ' . join(a:args) . '< /dev/null'
563 let out = system(cmd)
566 echo substitute(out, '\n*$', '', '')
574 " --- process and set the defaults {{{1
576 function! NM_set_defaults(force)
577 for [key, dflt] in items(s:notmuch_defaults)
579 if !a:force && exists(key) && type(dflt) == type(eval(key))
581 elseif type(dflt) == type(0)
582 let cmd = printf('let %s = %d', key, dflt)
583 elseif type(dflt) == type('')
584 let cmd = printf('let %s = ''%s''', key, dflt)
585 "elseif type(dflt) == type([])
586 " let cmd = printf('let %s = %s', key, string(dflt))
588 echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]',
589 \ a:force, key, string(dflt))
595 call NM_set_defaults(0)
597 " for some reason NM_set_defaults() didn't work for arrays...
598 if !exists('g:notmuch_show_headers')
599 let g:notmuch_show_headers = s:notmuch_show_headers_defaults
601 if !exists('g:notmuch_initial_search_words')
602 let g:notmuch_initial_search_words = s:notmuch_initial_search_words_defaults
606 " --- assign keymaps {{{1
608 function! s:NM_set_map(maps)
609 for [key, code] in items(a:maps)
610 exec printf('nnoremap <buffer> %s %s', key, code)
614 " --- command handler {{{1
616 function! NotMuch(args)
618 if exists('b:nm_search_words')
619 let words = b:nm_search_words
621 let words = g:notmuch_initial_search_words
623 call <SID>NM_cmd_search(words)
629 let words = split(a:args)
630 " TODO: handle commands passed as arguments
632 function! CompleteNotMuch(arg_lead, cmd_line, cursor_pos)
639 command! -nargs=* -complete=customlist,CompleteNotMuch NotMuch call NotMuch(<q-args>)
640 cabbrev notmuch <c-r>=(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch')<CR>
642 " --- hacks, only for development :) {{{1
644 nnoremap ,nmr :source ~/.vim/plugin/notmuch.vim<CR>:call NotMuch('')<CR>
646 " vim: set ft=vim ts=8 sw=8 et foldmethod=marker :