X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=vim%2Fplugin%2Fnotmuch.vim;h=0adca0b0c574922ec78a90a1e14a74d4a8b9105b;hp=8f6c7748ca5c1495c88c923f39cc9caa511bef93;hb=350e0136963d14d3dfe2ba88e80c217b483d5949;hpb=5a32a1d48f790d5bb63e5e7e70b989cd94692c0a diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim index 8f6c7748..0adca0b0 100644 --- a/vim/plugin/notmuch.vim +++ b/vim/plugin/notmuch.vim @@ -52,6 +52,10 @@ let s:notmuch_defaults = { \ '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 @@ -78,6 +82,24 @@ let s:notmuch_folders_defaults = [ \ [ '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 @@ -110,8 +132,8 @@ let g:notmuch_search_maps = { " --- --- bindings for show screen {{{2 let g:notmuch_show_maps = { - \ '': ':call NM_show_previous(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()', \ @@ -138,6 +160,16 @@ let g:notmuch_show_maps = { \ '?': ':echo NM_show_message_id() . '' @ '' . join(NM_show_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 @@ -156,12 +188,12 @@ function! s:NM_cmd_folders(words) call add(searches, search) endfor - call NM_newBuffer('folders', join(disp, "\n")) + 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(g:notmuch_folders_maps) + call NM_set_map('n', g:notmuch_folders_maps) setlocal cursorline setlocal nowrap endfunction @@ -204,12 +236,12 @@ function! s:NM_cmd_search(words) let disp = copy(lines) 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 @@ -254,7 +286,7 @@ function! s:NM_search_prompt() let tags = s:notmuch_initial_search_words_defaults endif let prev_bufnr = bufnr('%') - if b:nm_type == 'search' + 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 @@ -276,8 +308,8 @@ 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_add_remove_tags_on_screen('', '-', ['inbox']) + call NM_add_remove_tags([], '-', ['inbox']) norm j endfunction @@ -349,14 +381,16 @@ endfunction 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 + echo 'len=' . string(len(b:nm_raw_lines)) . ' mnum=' . string(mnum) + 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) @@ -370,8 +404,8 @@ 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 NM_add_remove_tags([], a:prefix, tags) + call NM_add_remove_tags_on_screen('', a:prefix, tags) endfunction " --- implement show screen {{{1 @@ -384,7 +418,7 @@ function! s:NM_cmd_show(words) 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_words = a:words let b:nm_raw_info = info @@ -392,56 +426,63 @@ function! s:NM_cmd_show(words) 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_previous(can_change_thread) +function! s:NM_show_previous(can_change_thread, find_matching) 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() norm G - call NM_show_previous(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() call NM_kill_this_buffer() if line('.') != line('$') norm j @@ -459,10 +500,6 @@ function! s:NM_show_mark_read_then_archive_thread() echo 'not implemented' endfunction -function! s:NM_show_next_message() - echo 'not implemented' -endfunction - function! s:NM_show_mark_read_then_next_open_message() echo 'not implemented' endfunction @@ -491,8 +528,65 @@ 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') + echo "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' + if len(ids) + call add(ids, 'OR') + endif + call add(ids, msg['id']) + endif + endfor + + let filter = ['('] + advance_tags + [')', 'AND', '('] + ids + [')'] + call NM_add_remove_tags(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') + echo "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 + call NM_add_remove_tags_on_screen(msg_top['start'], '-', advance_tags) + 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 = ['('] + advance_tags + [')', 'AND', msg_top['id']] + call NM_add_remove_tags(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() @@ -514,29 +608,30 @@ endfunction " --- --- show screen helper functions {{{2 -function! s:NM_show_message_id() - if !exists('b:nm_raw_info') - echoe 'no b:nm_raw_info' - return '' - endif - let info = b:nm_raw_info - let lnum = line('.') - for msg in info['msgs'] - if lnum > msg['end'] +function! s:NM_show_get_message_for_line(line) + for msg in b:nm_raw_info['msgs'] + if a:line > msg['end'] continue endif - if has_key(msg,'id') - return msg['id'] - endif - return '' + 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_search_words() if !exists('b:nm_words') - echoe 'no b:nm_words' - return [] + throw 'Eeek! no b:nm_words' endif return b:nm_words endfunction @@ -777,11 +872,154 @@ function! NM_cmd_show_foldtext() endfunction +" --- implement compose screen {{{1 + +function! s:NM_cmd_compose(words, body_lines) + let lines = !g:notmuch_compose_header_help ? [] : [ + \ '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', + \ ] + let start_on_line = 0 + + let hdrs = { } + for word in a:words + let m = matchlist(word, '^\([^:]\+\):\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 extend(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 add(lines, '') + 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 + + call NM_set_map('n', g:notmuch_compose_nmaps) + call NM_set_map('i', g:notmuch_compose_imaps) + + call cursor(start_on_line, strlen(start_on_line) + 1) + 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_compose_send() + echo 'not implemented' +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) + echo 'header end = ' . string(hdr_end) + 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() + let name = substitute(system('id -u -n'), '\v(^\s*|\s*$|\n)', '', 'g') + let fqdn = substitute(system('hostname -f'), '\v(^\s*|\s*$|\n)', '', 'g') + + " TODO: do this properly + return name . '@' . fqdn +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(type, 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 @@ -790,11 +1028,75 @@ function! s:NM_newBuffer(type, content) 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_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 words = a:args call map(words, 's:NM_shell_escape(v:val)') @@ -845,26 +1147,29 @@ function! s:NM_search_expand(arg) let b:nm_prev_bufnr = prev_bufnr endfunction -function! s:NM_add_remove_tags(prefix, tags) - let id = NM_search_thread_id() - if id == '' +function! s:NM_add_remove_tags(filter, prefix, tags) + let filter = len(a:filter) ? a:filter : [NM_search_thread_id()] + if !len(filter) echoe 'Eeek! I couldn''t find the thead id!' endif call map(a:tags, 'a:prefix . v:val') + let args = ['tag'] + call extend(args, a:tags) + call add(args, '--') + call extend(args, filter) " TODO: handle errors - call NM_run(['tag'] + a:tags + ['--', id]) + call NM_run(args) endfunction -function! s:NM_add_remove_tags_on_screen(prefix, tags) - let online = '' +function! s:NM_add_remove_tags_on_screen(online, prefix, tags) setlocal modifiable if a:prefix == '-' for tagname in a:tags - exec printf('silent %ss/(\([^)]*\)\<%s\>\([^)]*\))$/(\1\2)/', online, tagname) + exec printf('silent! %ss/(\([^)]*\)\<%s\>\([^)]*\))$/(\1\2)/', string(a:online), tagname) endfor else for tagname in a:tags - exec printf('silent %ss/(\([^)]*\)\([^)]*\))$/(\1 %s)/', online, tagname) + exec printf('silent! %ss/(\([^)]*\)\([^)]*\))$/(\1 %s)/', string(a:online), tagname) endfor endif setlocal nomodifiable @@ -905,13 +1210,19 @@ 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 :source ~/.vim/plugin/notmuch.vim:call NotMuch('') @@ -925,11 +1236,12 @@ function! NotMuch(args) let args = 'folders' endif - let words = split(args) - if words[0] == 'folders' + 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' + + elseif words[0] == 'search' || words[0] == 's' if len(words) > 1 let words = words[1:] elseif exists('b:nm_search_words') @@ -941,6 +1253,10 @@ function! NotMuch(args) elseif words[0] == 'show' echoe 'show is not yet implemented.' + + 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)