X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=vim%2Fplugin%2Fnotmuch.vim;h=6b82abedbfe2b9a655021bd6e7d99735f25356fa;hp=c45b8e15a782b9fa1d4f3829bd5b572ad216dcac;hb=e7899b00d0948f5d8e75b22b9ce82037593ebcdd;hpb=3e4f9009ae680d91b25b3ec2441c026acec3c174 diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim index c45b8e15..6b82abed 100644 --- a/vim/plugin/notmuch.vim +++ b/vim/plugin/notmuch.vim @@ -18,11 +18,15 @@ " along with Notmuch. If not, see . " " Authors: Bart Trojanowski - +" Contributors: Felipe Contreras , +" Peter Hartman +" " --- configuration defaults {{{1 let s:notmuch_defaults = { \ 'g:notmuch_cmd': 'notmuch' , + \ 'g:notmuch_sendmail': 'sendmail' , + \ 'g:notmuch_debug': 0 , \ \ 'g:notmuch_search_newest_first': 1 , \ 'g:notmuch_search_from_column_width': 20 , @@ -32,25 +36,29 @@ let s:notmuch_defaults = { \ 'g:notmuch_show_fold_bodies': 0 , \ 'g:notmuch_show_fold_headers': 1 , \ - \ 'g:notmuch_show_message_begin_regexp': '^ message{' , - \ 'g:notmuch_show_message_end_regexp': '^ message}' , - \ 'g:notmuch_show_header_begin_regexp': '^ header{' , - \ 'g:notmuch_show_header_end_regexp': '^ header}' , - \ 'g:notmuch_show_body_begin_regexp': '^ body{' , - \ 'g:notmuch_show_body_end_regexp': '^ body}' , - \ 'g:notmuch_show_attachment_begin_regexp': '^ attachment{' , - \ 'g:notmuch_show_attachment_end_regexp': '^ attachment}' , - \ 'g:notmuch_show_part_begin_regexp': '^ part{' , - \ 'g:notmuch_show_part_end_regexp': '^ part}' , - \ 'g:notmuch_show_marker_regexp': '^ \\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$', + \ 'g:notmuch_show_message_begin_regexp': ' message{' , + \ 'g:notmuch_show_message_end_regexp': ' message}' , + \ 'g:notmuch_show_header_begin_regexp': ' header{' , + \ 'g:notmuch_show_header_end_regexp': ' header}' , + \ 'g:notmuch_show_body_begin_regexp': ' body{' , + \ 'g:notmuch_show_body_end_regexp': ' body}' , + \ 'g:notmuch_show_attachment_begin_regexp': ' attachment{' , + \ 'g:notmuch_show_attachment_end_regexp': ' attachment}' , + \ 'g:notmuch_show_part_begin_regexp': ' part{' , + \ 'g:notmuch_show_part_end_regexp': ' part}' , + \ 'g:notmuch_show_marker_regexp': ' \\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$', \ - \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) filename:\(.*\)$', + \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) match:\([0-9]*\) filename:\(.*\)$', \ 'g:notmuch_show_tags_regexp': '(\([^)]*\))$' , \ \ 'g:notmuch_show_signature_regexp': '^\(-- \?\|_\+\)$' , \ 'g:notmuch_show_signature_lines_max': 12 , \ \ 'g:notmuch_show_citation_regexp': '^\s*>' , + \ + \ 'g:notmuch_compose_insert_mode_start': 1 , + \ 'g:notmuch_compose_header_help': 1 , + \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' , \ } " defaults for g:notmuch_initial_search_words @@ -69,35 +77,77 @@ let s:notmuch_show_headers_defaults = [ \ 'Date', \ ] +" defaults for g:notmuch_folders +" override with: let g:notmuch_folders = [ ... ] +let s:notmuch_folders_defaults = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ ] + +" defaults for g:notmuch_signature +" override with: let g:notmuch_signature = [ ... ] +let s:notmuch_signature_defaults = [ + \ '', + \ '-- ', + \ 'email sent from notmuch.vim plugin' + \ ] + +" defaults for g:notmuch_compose_headers +" override with: let g:notmuch_compose_headers = [ ... ] +let s:notmuch_compose_headers_defaults = [ + \ 'From', + \ 'To', + \ 'Cc', + \ 'Bcc', + \ 'Subject' + \ ] + " --- keyboard mapping definitions {{{1 +" --- --- bindings for folders mode {{{2 + +let g:notmuch_folders_maps = { + \ 'm': ':call NM_new_mail()', + \ 's': ':call NM_search_prompt()', + \ 'q': ':call NM_kill_this_buffer()', + \ '=': ':call NM_folders_refresh_view()', + \ '': ':call NM_folders_show_search()', + \ } + " --- --- bindings for search screen {{{2 let g:notmuch_search_maps = { - \ '': ':call NM_search_show_thread()', + \ '': ':call NM_search_show_thread(0)', + \ '': ':call NM_search_show_thread(1)', + \ '': ':call NM_search_expand('''')', \ 'a': ':call NM_search_archive_thread()', + \ 'A': ':call NM_search_mark_read_then_archive_thread()', \ 'f': ':call NM_search_filter()', \ 'm': ':call NM_new_mail()', \ 'o': ':call NM_search_toggle_order()', \ 'r': ':call NM_search_reply_to_thread()', \ 's': ':call NM_search_prompt()', - \ 'S': ':call NM_search_edit()', + \ ',s': ':call NM_search_edit()', \ 't': ':call NM_search_filter_by_tag()', \ 'q': ':call NM_kill_this_buffer()', \ '+': ':call NM_search_add_tags([])', \ '-': ':call NM_search_remove_tags([])', \ '=': ':call NM_search_refresh_view()', + \ '?': ':echo NM_search_thread_id() . '' @ '' . join(NM_get_search_words())', \ } " --- --- bindings for show screen {{{2 let g:notmuch_show_maps = { - \ '': ':call NM_show_prev(1)', - \ '': ':call NM_show_next(1)', + \ '': ':call NM_show_previous(1, 0)', + \ '': ':call NM_show_next(1, 0)', + \ '': ':call NM_search_expand('''')', \ 'q': ':call NM_kill_this_buffer()', + \ 's': ':call NM_search_prompt()', \ \ 'b': ':call NM_show_fold_toggle(''b'', ''bdy'', !g:notmuch_show_fold_bodies)', \ 'c': ':call NM_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)', \ 'h': ':call NM_show_fold_toggle(''h'', ''hdr'', !g:notmuch_show_fold_headers)', - \ 's': ':call NM_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)', + \ 'i': ':call NM_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)', \ \ 'a': ':call NM_show_archive_thread()', \ 'A': ':call NM_show_mark_read_then_archive_thread()', @@ -108,10 +158,76 @@ let g:notmuch_show_maps = { \ '': ':call NM_show_advance_marking_read_and_archiving()', \ '\|': ':call NM_show_pipe_message()', \ + \ '': ':call NM_show_previous_fold()', + \ '': ':call NM_show_next_fold()', + \ '': ':call NM_show_toggle_fold()', + \ \ 'r': ':call NM_show_reply()', \ 'm': ':call NM_new_mail()', + \ '?': ':echo NM_show_message_id() . '' @ '' . join(NM_get_search_words())', + \ } + +" --- --- bindings for compose screen {{{2 +let g:notmuch_compose_nmaps = { + \ ',s': ':call NM_compose_send()', + \ ',a': ':call NM_compose_attach()', + \ ',q': ':call NM_kill_this_buffer()', + \ '': ':call NM_compose_next_entry_area()', + \ } +let g:notmuch_compose_imaps = { + \ '': '=NM_compose_next_entry_area()', \ } +" --- implement folders screen {{{1 + +function! s:NM_cmd_folders(words) + if len(a:words) + throw 'Not expecting any arguments for folders command.' + endif + let cmd = ['count'] + let disp = [] + let searches = [] + for entry in g:notmuch_folders + let [ name, search ] = entry + let data = s:NM_run(cmd + [search]) + let cnt = matchlist(data, '\(\d\+\)')[1] + call add(disp, printf('%9d %-20s (%s)', cnt, name, search)) + call add(searches, search) + endfor + + call NM_newBuffer('', 'folders', join(disp, "\n")) + let b:nm_searches = searches + let b:nm_timestamp = reltime() + + call NM_cmd_folders_mksyntax() + call NM_set_map('n', g:notmuch_folders_maps) + setlocal cursorline + setlocal nowrap +endfunction + +function! s:NM_cmd_folders_mksyntax() +endfunction + +" --- --- folders screen action functions {{{2 + +function! s:NM_folders_refresh_view() + let lno = line('.') + setlocal bufhidden=delete + call s:NM_cmd_folders([]) + exec printf('norm %dG', lno) +endfunction + +function! s:NM_folders_show_search() + let line = line('.') + let search = b:nm_searches[line-1] + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_cmd_search([search]) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr +endfunction + " --- implement search screen {{{1 @@ -123,49 +239,44 @@ function! s:NM_cmd_search(words) let cmd = cmd + ['--sort=oldest-first'] endif let data = s:NM_run(cmd + a:words) - "let data = substitute(data, '27/27', '25/27', '') - "let data = substitute(data, '\[4/4\]', '[0/4]', '') let lines = split(data, "\n") let disp = copy(lines) - "call map(disp, 'substitute(v:val, "^thread:\\S* ", "", "")' ) call map(disp, 's:NM_cmd_search_fmtline(v:val)') - call NM_newBuffer('search', join(disp, "\n")) + call NM_newBuffer('', 'search', join(disp, "\n")) let b:nm_raw_lines = lines let b:nm_search_words = a:words - call NM_cmd_search_mksyntax() - call NM_set_map(g:notmuch_search_maps) + call NM_set_map('n', g:notmuch_search_maps) setlocal cursorline setlocal nowrap endfunction function! s:NM_cmd_search_fmtline(line) - let m = matchlist(a:line, '^\(thread:\S\+\)\s\([^]]\+\]\) \([^;]\+\); \(.*\) (\([^(]*\))$') + let m = matchlist(a:line, '^\(thread:\S\+\)\s\(.\{12\}\) \[\(\d\+\)/\d\+\] \([^;]\+\); \%(\[[^\[]\+\] \)*\(.*\) (\([^(]*\))$') if !len(m) return 'ERROR PARSING: ' . a:line endif let max = g:notmuch_search_from_column_width - let from = m[3] - if strlen(from) >= max - let from = m[3][0:max-4] . '...' - endif - return printf('%s %-20s | %s (%s)', m[2], from, m[4], m[5]) -endfunction -function! s:NM_cmd_search_mksyntax() - syntax clear nmSearchFrom - "syntax region nmSearchFrom start='\]\@<=' end='.'me=e+5,he=e+5,re=e+5 oneline contained - "syntax match nmSearchFrom /\]\@<=.\{10\}/ oneline contained - exec printf('syntax match nmSearchFrom /\(\] \)\@<=.\{%d\}/ oneline contained', g:notmuch_search_from_column_width) - "exec printf('syntax region nmSearchFrom start=''\%%%dv'' end=''\%%%dv'' oneline contained', 20, 30) + let flist = [] + for at in split(m[4], ", ") + let p = min([stridx(at, "."), stridx(at, "@")]) + call insert(flist, tolower(at[0:p - 1])) + endfor + let from = join(flist, ", ") + return printf("%-12s %3s %-20.20s | %s (%s)", m[2], m[3], from, m[5], m[6]) endfunction " --- --- search screen action functions {{{2 -function! s:NM_search_show_thread() - let id = NM_search_find_thread_id() - if id != '' - call NM_cmd_show([id]) +function! s:NM_search_show_thread(everything) + let words = [ NM_search_thread_id() ] + if !a:everything && exists('b:nm_search_words') + call extend(words, ['AND', '(']) + call extend(words, b:nm_search_words) + call add(words, ')') endif + call NM_cmd_show(words) + let b:nm_show_everything = a:everything endfunction function! s:NM_search_prompt() @@ -176,8 +287,18 @@ function! s:NM_search_prompt() else let tags = s:notmuch_initial_search_words_defaults endif - setlocal bufhidden=delete + let prev_bufnr = bufnr('%') + if b:nm_type == 'search' && exists('b:nm_prev_bufnr') + " TODO: we intend to replace the current buffer, + " ... maybe we could just clear it + let prev_bufnr = b:nm_prev_bufnr + setlocal bufhidden=delete + else + setlocal bufhidden=hide + endif call NM_cmd_search(tags) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr endfunction function! s:NM_search_edit() @@ -189,30 +310,32 @@ function! s:NM_search_edit() endfunction function! s:NM_search_archive_thread() - call NM_add_remove_tags_on_screen('-', ['inbox']) - call NM_add_remove_tags('-', ['inbox']) + call NM_tag([], ['-inbox']) + norm j +endfunction + +function! s:NM_search_mark_read_then_archive_thread() + call NM_tag([], ['-unread', '-inbox']) norm j endfunction function! s:NM_search_filter() - call NM_search_filter_helper('Filter: ', '') + call NM_search_filter_helper('Filter: ', '', '') endfunction function! s:NM_search_filter_by_tag() - call NM_search_filter_helper('Filter Tag(s): ', 'tag:') + call NM_search_filter_helper('Filter Tag(s): ', 'tag:', 'and') endfunction -function! s:NM_search_filter_helper(prompt, prefix) +function! s:NM_search_filter_helper(prompt, prefix, joiner) " TODO: input() can support completion - let text = input(a:prompt) + let text = substitute(input(a:prompt), '\v(^\s*|\s*$|\n)', '', 'g') if !strlen(text) return endif - let tags = split(text) - map(tags, 'and a:prefix . v:val') - let tags = b:nm_search_words + tags - echo tags + let tags = b:nm_search_words + ['AND'] + \ + NM_combine_tags(a:prefix, split(text), a:joiner, '()') let prev_bufnr = bufnr('%') setlocal bufhidden=hide @@ -229,7 +352,14 @@ function! s:NM_search_toggle_order() endfunction function! s:NM_search_reply_to_thread() - echo 'not implemented' + let cmd = ['reply'] + call add(cmd, NM_search_thread_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) endfunction function! s:NM_search_add_tags(tags) @@ -242,24 +372,27 @@ endfunction function! s:NM_search_refresh_view() let lno = line('.') + let prev_bufnr = b:nm_prev_bufnr setlocal bufhidden=delete call NM_cmd_search(b:nm_search_words) + let b:nm_prev_bufnr = prev_bufnr " FIXME: should find the line of the thread we were on if possible exec printf('norm %dG', lno) endfunction " --- --- search screen helper functions {{{2 -function! s:NM_search_find_thread_id() +function! s:NM_search_thread_id() if !exists('b:nm_raw_lines') - echoe 'no b:nm_raw_lines' + throw 'Eeek! no b:nm_raw_lines' + endif + let mnum = line('.') - 1 + if len(b:nm_raw_lines) <= mnum return '' - else - let line = line('.') - let info = b:nm_raw_lines[line-1] - let what = split(info, '\s\+')[0] - return what endif + let info = b:nm_raw_lines[mnum] + let what = split(info, '\s\+')[0] + return what endfunction function! s:NM_search_add_remove_tags(prompt, prefix, intags) @@ -273,96 +406,104 @@ function! s:NM_search_add_remove_tags(prompt, prefix, intags) else let tags = a:intags endif - call NM_add_remove_tags(a:prefix, tags) - call NM_add_remove_tags_on_screen(a:prefix, tags) + call map(tags, 'a:prefix . v:val') + call NM_tag([], tags) endfunction " --- implement show screen {{{1 function! s:NM_cmd_show(words) let prev_bufnr = bufnr('%') - let data = s:NM_run(['show'] + a:words) + let data = s:NM_run(['show', '--entire-thread'] + a:words) let lines = split(data, "\n") let info = s:NM_cmd_show_parse(lines) setlocal bufhidden=hide - call NM_newBuffer('show', join(info['disp'], "\n")) + call NM_newBuffer('', 'show', join(info['disp'], "\n")) setlocal bufhidden=delete + let b:nm_search_words = a:words let b:nm_raw_info = info let b:nm_prev_bufnr = prev_bufnr call NM_cmd_show_mkfolds() call NM_cmd_show_mksyntax() - call NM_set_map(g:notmuch_show_maps) + call NM_set_map('n', g:notmuch_show_maps) setlocal foldtext=NM_cmd_show_foldtext() setlocal fillchars= setlocal foldcolumn=6 endfunction -function! s:NM_show_prev(can_change_thread) +function! s:NM_show_previous(can_change_thread, find_matching) + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 let info = b:nm_raw_info let lnum = line('.') for msg in reverse(copy(info['msgs'])) + if a:find_matching && msg['match'] == '0' + continue + endif if lnum <= msg['start'] continue endif - exec printf('norm %dG', msg['start']) + exec printf('norm %dGzt', msg['start']) " TODO: try to fit the message on screen - norm zz return endfor if !a:can_change_thread return endif call NM_kill_this_buffer() - if line('.') != line('0') + if line('.') > 1 norm k - call NM_search_show_thread() + call NM_search_show_thread(everything) norm G - call NM_show_prev(0) + call NM_show_previous(0, a:find_matching) else echo 'No more messages.' endif endfunction -function! s:NM_show_next(can_change_thread) +function! s:NM_show_next(can_change_thread, find_matching) let info = b:nm_raw_info let lnum = line('.') for msg in info['msgs'] + if a:find_matching && msg['match'] == '0' + continue + endif if lnum >= msg['start'] continue endif - exec printf('norm %dG', msg['start']) + exec printf('norm %dGzt', msg['start']) " TODO: try to fit the message on screen - norm zz return endfor - if !a:can_change_thread - return + if a:can_change_thread + call NM_show_next_thread() endif +endfunction + +function! s:NM_show_next_thread() + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 call NM_kill_this_buffer() if line('.') != line('$') norm j - call NM_search_show_thread() + call NM_search_show_thread(everything) else echo 'No more messages.' endif endfunction function! s:NM_show_archive_thread() - echo 'not implemented' + call NM_tag(b:nm_search_words, ['-inbox']) + call NM_show_next_thread() endfunction function! s:NM_show_mark_read_then_archive_thread() - echo 'not implemented' -endfunction - -function! s:NM_show_next_message() - echo 'not implemented' + call NM_tag(b:nm_search_words, ['-unread', '-inbox']) + call NM_show_next_thread() endfunction function! s:NM_show_mark_read_then_next_open_message() @@ -374,7 +515,14 @@ function! s:NM_show_previous_message() endfunction function! s:NM_show_reply() - echo 'not implemented' + let cmd = ['reply'] + call add(cmd, NM_show_message_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) endfunction function! s:NM_show_view_all_mime_parts() @@ -393,15 +541,106 @@ function! s:NM_show_remove_tag() echo 'not implemented' endfunction +" if entire message is not visible scroll down 1/2 page or less to get to the bottom of message +" otherwise go to next message +" any message that is viewed entirely has inbox and unread tags removed function! s:NM_show_advance_marking_read_and_archiving() - echo 'not implemented' + let advance_tags = ['unread', 'inbox'] + + let vis_top = line('w0') + let vis_bot = line('w$') + + let msg_top = NM_show_get_message_for_line(vis_top) + if !has_key(msg_top,'id') + throw "No top visible message." + endif + + " if the top message is the last message, just expunge the entire thread and move on + if msg_top['end'] == line('$') + let ids = [] + for msg in b:nm_raw_info['msgs'] + if has_key(msg,'match') && msg['match'] != '0' + call add(ids, msg['id']) + endif + endfor + let filter = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND'] + \ + NM_combine_tags('', ids, 'OR', '()') + call map(advance_tags, '"+" . v:val') + call NM_tag(filter, advance_tags) + call NM_show_next(1, 1) + return + endif + + let msg_bot = NM_show_get_message_for_line(vis_bot) + if !has_key(msg_bot,'id') + throw "No bottom visible message." + endif + + " if entire message fits on the screen, read/archive it, move to the next one + if msg_top['id'] != msg_bot['id'] || msg_top['end'] <= vis_bot + exec printf('norm %dG', vis_top) + call NM_show_next(0, 1) + if has_key(msg_top,'match') && msg_top['match'] != '0' + redraw + " do this last to hide the latency + let filter = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND', msg_top['id']] + call map(advance_tags, '"-" . v:val') + call NM_tag(filter, advance_tags) + endif + return + endif + + " entire message does not fit on the screen, scroll down to bottom, max 1/2 screen + let jmp = winheight(winnr()) / 2 + let max = msg_bot['end'] - vis_bot + if jmp > max + let jmp = max + endif + exec printf('norm %dGzt', vis_top + jmp) + return endfunction function! s:NM_show_pipe_message() echo 'not implemented' endfunction -" --- --- search screen helper functions {{{2 +function! s:NM_show_previous_fold() + echo 'not implemented' +endfunction + +function! s:NM_show_next_fold() + echo 'not implemented' +endfunction + +function! s:NM_show_toggle_fold() + echo 'not implemented' +endfunction + + +" --- --- show screen helper functions {{{2 + +function! s:NM_show_get_message_for_line(line) + for msg in b:nm_raw_info['msgs'] + if a:line > msg['end'] + continue + endif + return msg + endfor + return {} +endfunction + +function! s:NM_show_message_id() + if !exists('b:nm_raw_info') + throw 'Eeek! no b:nm_raw_info' + endif + let msg = NM_show_get_message_for_line(line('.')) + if has_key(msg,'id') + return msg['id'] + endif + return '' +endfunction function! s:NM_show_fold_toggle(key, type, fold) let info = b:nm_raw_info @@ -410,9 +649,19 @@ function! s:NM_show_fold_toggle(key, type, fold) let act = 'close' endif for fld in info['folds'] - if fld[0] == a:type - exec printf('%dfold%s', fld[1], act) + if fld[0] != a:type + continue + endif + "let idx = fld[3] + "let msg = info['msgs'][idx] + "if has_key(msg,'match') && msg['match'] == '0' + " continue + "endif + let cls = foldclosed(fld[1]) + if cls != -1 && cls != fld[1] + continue endif + exec printf('%dfold%s', fld[1], act) endfor exec printf('nnoremap %s :call NM_show_fold_toggle(''%s'', ''%s'', %d)', a:key, a:key, a:type, !a:fold) endfunction @@ -468,7 +717,7 @@ function! s:NM_cmd_show_parse(inlines) elseif mode_type == 'cit' if part_end || match(line, g:notmuch_show_citation_regexp) == -1 let outlnum = len(info['disp']) - let foldinfo = [ mode_type, mode_start, outlnum-1, + let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']), \ printf('[ %d-line citation. Press "c" to show. ]', outlnum - mode_start) ] let mode_type = '' endif @@ -477,7 +726,7 @@ function! s:NM_cmd_show_parse(inlines) if (outlnum - mode_start) > g:notmuch_show_signature_lines_max let mode_type = '' elseif part_end - let foldinfo = [ mode_type, mode_start, outlnum, + let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']), \ printf('[ %d-line signature. Press "s" to show. ]', outlnum - mode_start) ] let mode_type = '' endif @@ -487,12 +736,12 @@ function! s:NM_cmd_show_parse(inlines) if part_end " FIXME: this is a hack for handling two folds being added for one line " we should handle addinga fold in a function - if len(foldinfo) - call add(info['folds'], foldinfo[0:2]) - let info['foldtext'][foldinfo[1]] = foldinfo[3] + if len(foldinfo) && foldinfo[1] < foldinfo[2] + call add(info['folds'], foldinfo[0:3]) + let info['foldtext'][foldinfo[1]] = foldinfo[4] endif - let foldinfo = [ 'text', part_start, part_end, + let foldinfo = [ 'text', part_start, part_end, len(info['msgs']), \ printf('[ %d-line %s. Press "p" to show. ]', part_end - part_start, in_part) ] let in_part = '' call add(info['disp'], '') @@ -504,7 +753,7 @@ function! s:NM_cmd_show_parse(inlines) endif if match(line, g:notmuch_show_body_end_regexp) != -1 let body_end = len(info['disp']) - let foldinfo = [ 'bdy', body_start, body_end, + let foldinfo = [ 'bdy', body_start, body_end, len(info['msgs']), \ printf('[ BODY %d - %d lines ]', len(info['msgs']), body_end - body_start) ] let in_body = 0 @@ -531,8 +780,8 @@ function! s:NM_cmd_show_parse(inlines) if match(line, g:notmuch_show_header_end_regexp) != -1 let hdr_start = msg['hdr_start']+1 let hdr_end = len(info['disp']) - let foldinfo = [ 'hdr', hdr_start, hdr_end, - \ printf('[ %d-line headers. Press "h" to show. ]', hdr_end - hdr_start) ] + let foldinfo = [ 'hdr', hdr_start, hdr_end, len(info['msgs']), + \ printf('[ %d-line headers. Press "h" to show. ]', hdr_end + 1 - hdr_start) ] let msg['header'] = hdr let in_header = 0 let hdr = {} @@ -552,7 +801,7 @@ function! s:NM_cmd_show_parse(inlines) let msg['end'] = len(info['disp']) call add(info['disp'], '') - let foldinfo = [ 'msg', msg['start'], msg['end'], + let foldinfo = [ 'msg', msg['start'], msg['end'], len(info['msgs']), \ printf('[ MSG %d - %s ]', len(info['msgs']), msg['descr']) ] call add(info['msgs'], msg) @@ -580,16 +829,17 @@ function! s:NM_cmd_show_parse(inlines) if len(m) let msg['id'] = m[1] let msg['depth'] = m[2] - let msg['filename'] = m[3] + let msg['match'] = m[3] + let msg['filename'] = m[4] endif let in_message = 1 endif endif - if len(foldinfo) - call add(info['folds'], foldinfo[0:2]) - let info['foldtext'][foldinfo[1]] = foldinfo[3] + if len(foldinfo) && foldinfo[1] < foldinfo[2] + call add(info['folds'], foldinfo[0:3]) + let info['foldtext'][foldinfo[1]] = foldinfo[4] endif endfor return info @@ -600,14 +850,20 @@ function! s:NM_cmd_show_mkfolds() for afold in info['folds'] exec printf('%d,%dfold', afold[1], afold[2]) + let state = 'open' if (afold[0] == 'sig' && g:notmuch_show_fold_signatures) \ || (afold[0] == 'cit' && g:notmuch_show_fold_citations) \ || (afold[0] == 'bdy' && g:notmuch_show_fold_bodies) \ || (afold[0] == 'hdr' && g:notmuch_show_fold_headers) - exec printf('%dfoldclose', afold[1]) - else - exec printf('%dfoldopen', afold[1]) + let state = 'close' + elseif afold[0] == 'msg' + let idx = afold[3] + let msg = info['msgs'][idx] + if has_key(msg,'match') && msg['match'] == '0' + let state = 'close' + endif endif + exec printf('%dfold%s', afold[1], state) endfor endfunction @@ -632,27 +888,321 @@ function! NM_cmd_show_foldtext() endfunction +" --- implement compose screen {{{1 + +function! s:NM_cmd_compose(words, body_lines) + let lines = [] + let start_on_line = 0 + + let hdrs = { } + for word in a:words + let m = matchlist(word, '^\(\w[^:]*\):\s*\(.*\)\s*$') + if !len(m) + throw 'Eeek! bad parameter ''' . string(word) . '''' + endif + let key = substitute(m[1], '\<\w', '\U&', 'g') + if !has_key(hdrs, key) + let hdrs[key] = [] + endif + if strlen(m[2]) + call add(hdrs[key], m[2]) + endif + endfor + + if !has_key(hdrs, 'From') || !len(hdrs['From']) + let me = NM_compose_get_user_email() + let hdrs['From'] = [ me ] + endif + + for key in g:notmuch_compose_headers + let text = has_key(hdrs, key) ? join(hdrs[key], ', ') : '' + call add(lines, key . ': ' . text) + if !start_on_line && !strlen(text) + let start_on_line = len(lines) + endif + endfor + + for [key,val] in items(hdrs) + if match(g:notmuch_compose_headers, key) == -1 + let line = key . ': ' . join(val, ', ') + call add(lines, line) + endif + endfor + + call add(lines, '') + if !start_on_line + let start_on_line = len(lines) + 1 + endif + + if len(a:body_lines) + call extend(lines, a:body_lines) + else + call extend(lines, [ '', '' ]) + endif + + call NM_newComposeBuffer(lines, start_on_line) +endfunction + +function! s:NM_compose_send() + call NM_assert_buffer_type('compose') + let fname = expand('%') + let lnum = 1 + let line = getline(lnum) + let lst_hdr = '' + while match(line, '^$') == -1 + if !exists("hdr_starts") && match(line, '^Notmuch-Help:') == -1 + let hdr_starts = lnum - 1 + endif + let lnum = lnum + 1 + let line = getline(lnum) + endwhile + let body_starts = lnum - 1 + + call append(body_starts, 'Date: ' . strftime('%a, %d %b %Y %H:%M:%S %z')) + exec printf(':0,%dd', hdr_starts) + write + + let cmdtxt = g:notmuch_sendmail . ' -t < ' . fname + let out = system(cmdtxt) + let err = v:shell_error + if err + undo + write + call NM_newBuffer('new', 'error', + \ "While running...\n" . + \ ' ' . cmdtxt . "\n" . + \ "\n" . + \ "Failed with...\n" . + \ substitute(out, '^', ' ', 'g')) + echohl Error + echo 'Eeek! unable to send mail' + echohl None + return + endif + + if !exists('b:nm_prev_bufnr') + bdelete + else + let prev_bufnr = b:nm_prev_bufnr + bdelete + if prev_bufnr == bufnr('%') + exec printf("buffer %d", prev_bufnr) + endif + endif + call delete(fname) + echo 'Mail sent successfully.' +endfunction + +function! s:NM_compose_attach() + echo 'not implemented' +endfunction + +function! s:NM_compose_next_entry_area() + let lnum = line('.') + let hdr_end = NM_compose_find_line_match(1,'^$',1) + if lnum < hdr_end + let lnum = lnum + 1 + let line = getline(lnum) + if match(line, '^\([^:]\+\):\s*$') == -1 + call cursor(lnum, strlen(line) + 1) + return '' + endif + while match(getline(lnum+1), '^\s') != -1 + let lnum = lnum + 1 + endwhile + call cursor(lnum, strlen(getline(lnum)) + 1) + return '' + + elseif lnum == hdr_end + call cursor(lnum+1, strlen(getline(lnum+1)) + 1) + return '' + endif + if mode() == 'i' + if !getbufvar(bufnr('.'), '&et') + return "\t" + endif + let space = '' + let shiftwidth = a:shiftwidth + let shiftwidth = shiftwidth - ((virtcol('.')-1) % shiftwidth) + " we assume no one has shiftwidth set to more than 40 :) + return ' '[0:shiftwidth] + endif +endfunction + +" --- --- compose screen helper functions {{{2 + +function! s:NM_compose_get_user_email() + " TODO: do this properly (still), i.e., allow for multiple email accounts + let email = substitute(system('notmuch config get user.primary_email'), '\v(^\s*|\s*$|\n)', '', 'g') + return email +endfunction + +function! s:NM_compose_find_line_match(start, pattern, failure) + let lnum = a:start + let lend = line('$') + while lnum < lend + if match(getline(lnum), a:pattern) != -1 + return lnum + endif + let lnum = lnum + 1 + endwhile + return a:failure +endfunction + + " --- notmuch helper functions {{{1 -function! s:NM_newBuffer(ft, content) - enew - setlocal buftype=nofile readonly modifiable +function! s:NM_newBuffer(how, type, content) + if strlen(a:how) + exec a:how + else + enew + endif + setlocal buftype=nofile readonly modifiable scrolloff=0 sidescrolloff=0 silent put=a:content keepjumps 0d setlocal nomodifiable - execute printf('set filetype=notmuch-%s', a:ft) - execute printf('set syntax=notmuch-%s', a:ft) + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + let b:nm_type = a:type +endfunction + +function! s:NM_newFileBuffer(fdir, fname, type, lines) + let fdir = expand(a:fdir) + if !isdirectory(fdir) + call mkdir(fdir, 'p') + endif + let file_name = NM_mktemp(fdir, a:fname) + if writefile(a:lines, file_name) + throw 'Eeek! couldn''t write to temporary file ' . file_name + endif + exec printf('edit %s', file_name) + setlocal buftype= noreadonly modifiable scrolloff=0 sidescrolloff=0 + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + let b:nm_type = a:type +endfunction + +function! s:NM_newComposeBuffer(lines, start_on_line) + let lines = a:lines + let start_on_line = a:start_on_line + let real_hdr_start = 1 + if g:notmuch_compose_header_help + let help_lines = [ + \ 'Notmuch-Help: Type in your message here; to help you use these bindings:', + \ 'Notmuch-Help: ,a - attach a file', + \ 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)', + \ 'Notmuch-Help: ,q - abort the message', + \ 'Notmuch-Help: - skip through header lines', + \ ] + call extend(lines, help_lines, 0) + let real_hdr_start = len(help_lines) + if start_on_line > 0 + let start_on_line = start_on_line + len(help_lines) + endif + endif + call extend(lines, g:notmuch_signature) + + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail', + \ 'compose', lines) + setlocal bufhidden=hide + let b:nm_prev_bufnr = prev_bufnr + + call NM_set_map('n', g:notmuch_compose_nmaps) + call NM_set_map('i', g:notmuch_compose_imaps) + + if start_on_line > 0 && start_on_line <= len(lines) + call cursor(start_on_line, strlen(getline(start_on_line)) + 1) + else + call cursor(real_hdr_start, strlen(getline(real_hdr_start)) + 1) + call NM_compose_next_entry_area() + endif + + if g:notmuch_compose_insert_mode_start + startinsert! + endif + echo 'Type your message, use to jump to next header and then body.' endfunction +function! s:NM_assert_buffer_type(type) + if !exists('b:nm_type') || b:nm_type != a:type + throw printf('Eeek! expected type %s, but got %s.', a:type, + \ exists(b:nm_type) ? b:nm_type : 'something else') + endif +endfunction + +function! s:NM_mktemp(dir, name) + let time_stamp = strftime('%Y%m%d-%H%M%S') + let file_name = substitute(a:dir,'/*$','/','') . printf(a:name, time_stamp) + " TODO: check if it exists, try again + return file_name +endfunction + +function! s:NM_shell_escape(word) + " TODO: use shellescape() + let word = substitute(a:word, '''', '\\''', 'g') + return '''' . word . '''' +endfunction + +" this function was taken from git.vim, then fixed up +" http://github.com/motemen/git-vim +function! s:NM_shell_split(cmd) + let l:split_cmd = [] + let cmd = a:cmd + let iStart = 0 + while 1 + let t = match(cmd, '\S', iStart) + if t < iStart + break + endif + let iStart = t + + let iSpace = match(cmd, '\v(\s|$)', iStart) + if iSpace < iStart + break + endif + + let iQuote1 = match(cmd, '\(^["'']\|[^\\]\@<=["'']\)', iStart) + if iQuote1 > iSpace || iQuote1 < iStart + let iEnd = iSpace - 1 + let l:split_cmd += [ cmd[iStart : iEnd] ] + else + let q = cmd[iQuote1] + let iQuote2 = match(cmd, '[^\\]\@<=[' . q . ']', iQuote1 + 1) + if iQuote2 < iQuote1 + throw 'No matching ' . q . ' quote' + endif + let iEnd = iQuote2 + let l:split_cmd += [ cmd[iStart+1 : iEnd-1 ] ] + endif + + + let iStart = iEnd + 1 + endwhile + + return l:split_cmd +endfunction + + function! s:NM_run(args) - let cmd = g:notmuch_cmd . ' ' . join(a:args) . '< /dev/null' + let words = a:args + call map(words, 's:NM_shell_escape(v:val)') + let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null' - let start = reltime() - let out = system(cmd) - let err = v:shell_error - let delta = reltime(start) + if exists('g:notmuch_debug') && g:notmuch_debug + let start = reltime() + let out = system(cmd) + let err = v:shell_error + let delta = reltime(start) - echo printf('[%s] {%s} %s', reltimestr(delta), string(err), string(cmd)) + echo printf('[%s] {%s} %s', reltimestr(delta), string(err), string(cmd)) + else + let out = system(cmd) + let err = v:shell_error + endif if err echohl Error @@ -667,43 +1217,72 @@ endfunction " --- external mail handling helpers {{{1 function! s:NM_new_mail() - echo 'not implemented' + call NM_cmd_compose([], []) +endfunction + +" --- tag manipulation helpers {{{1 + +" used to combine an array of words with prefixes and separators +" example: +" NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()') +" -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')'] +function! s:NM_combine_tags(word_prefix, words, separator, brackets) + let res = [] + for word in a:words + if len(res) && strlen(a:separator) + call add(res, a:separator) + endif + call add(res, a:word_prefix . word) + endfor + if len(res) > 1 && strlen(a:brackets) + if strlen(a:brackets) != 2 + throw 'Eeek! brackets arg to NM_combine_tags must be 2 chars' + endif + call insert(res, a:brackets[0]) + call add(res, a:brackets[1]) + endif + return res endfunction " --- other helpers {{{1 +function! s:NM_get_search_words() + if !exists('b:nm_search_words') + throw 'Eeek! no b:nm_search_words' + endif + return b:nm_search_words +endfunction + function! s:NM_kill_this_buffer() if exists('b:nm_prev_bufnr') - setlocal bufhidden=delete - exec printf(":buffer %d", b:nm_prev_bufnr) + let prev_bufnr = b:nm_prev_bufnr + bdelete! + exec printf("buffer %d", prev_bufnr) else - echo "Nothing to kill." + echo "This is the last buffer; use :q to quit." endif endfunction -function! s:NM_add_remove_tags(prefix, tags) - let id = NM_search_find_thread_id() - if id == '' - echoe 'Eeek! I couldn''t find the thead id!' - endif - call map(a:tags, 'a:prefix . v:val') - " TODO: handle errors - call NM_run(['tag'] + a:tags + ['--', id]) +function! s:NM_search_expand(arg) + let word = expand(a:arg) + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_cmd_search([word]) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr endfunction -function! s:NM_add_remove_tags_on_screen(prefix, tags) - let online = '' - setlocal modifiable - if a:prefix == '-' - for tagname in a:tags - exec printf('silent %ss/(\([^)]*\)\<%s\>\([^)]*\))$/(\1\2)/', online, tagname) - endfor - else - for tagname in a:tags - exec printf('silent %ss/(\([^)]*\)\([^)]*\))$/(\1 %s)/', online, tagname) - endfor +function! s:NM_tag(filter, tags) + let filter = len(a:filter) ? a:filter : [NM_search_thread_id()] + if !len(filter) + throw 'Eeek! I couldn''t find the thead id!' endif - setlocal nomodifiable + let args = ['tag'] + call extend(args, a:tags) + call add(args, '--') + call extend(args, filter) + " TODO: handle errors + call NM_run(args) endfunction " --- process and set the defaults {{{1 @@ -717,6 +1296,7 @@ function! NM_set_defaults(force) let cmd = printf('let %s = %d', key, dflt) elseif type(dflt) == type('') let cmd = printf('let %s = ''%s''', key, dflt) + " FIXME: not sure why this didn't work when dflt is an array "elseif type(dflt) == type([]) " let cmd = printf('let %s = %s', key, string(dflt)) else @@ -736,33 +1316,58 @@ endif if !exists('g:notmuch_initial_search_words') let g:notmuch_initial_search_words = s:notmuch_initial_search_words_defaults endif +if !exists('g:notmuch_folders') + let g:notmuch_folders = s:notmuch_folders_defaults +endif +if !exists('g:notmuch_signature') + let g:notmuch_signature = s:notmuch_signature_defaults +endif +if !exists('g:notmuch_compose_headers') + let g:notmuch_compose_headers = s:notmuch_compose_headers_defaults +endif " --- assign keymaps {{{1 -function! s:NM_set_map(maps) +function! s:NM_set_map(type, maps) + nmapclear for [key, code] in items(a:maps) - exec printf('nnoremap %s %s', key, code) + exec printf('%snoremap %s %s', a:type, key, code) endfor + " --- this is a hack for development :) + nnoremap ,nmr :runtime! plugin/notmuch.vim endfunction " --- command handler {{{1 function! NotMuch(args) - if !strlen(a:args) - if exists('b:nm_search_words') + let args = a:args + if !strlen(args) + let args = 'folders' + endif + + let words = NM_shell_split(args) + if words[0] == 'folders' || words[0] == 'f' + let words = words[1:] + call NM_cmd_folders(words) + + elseif words[0] == 'search' || words[0] == 's' + if len(words) > 1 + let words = words[1:] + elseif exists('b:nm_search_words') let words = b:nm_search_words else let words = g:notmuch_initial_search_words endif call NM_cmd_search(words) - return - endif - echo "blarg!" + elseif words[0] == 'show' + echoe 'show is not yet implemented.' - let words = split(a:args) - " TODO: handle commands passed as arguments + elseif words[0] == 'new' || words[0] == 'compose' + let words = words[1:] + call NM_cmd_compose(words, []) + endif endfunction function! CompleteNotMuch(arg_lead, cmd_line, cursor_pos) return [] @@ -774,8 +1379,4 @@ endfunction command! -nargs=* -complete=customlist,CompleteNotMuch NotMuch call NotMuch() cabbrev notmuch =(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch') -" --- hacks, only for development :) {{{1 - -nnoremap ,nmr :source ~/.vim/plugin/notmuch.vim:call NotMuch('') - " vim: set ft=vim ts=8 sw=8 et foldmethod=marker :