[RFC/PATCH] Vim client rewrite

Subject: [RFC/PATCH] Vim client rewrite

Date: Sun, 15 May 2011 21:15:31 +0200

To: notmuch@notmuchmail.org

Cc:

From: anton@khirnov.net


Hi,

my attempts to make the vim client more usable somehow spiraled out of
control and turned into a huge rewrite. The intermediate results I
hereby present for your amusement and comments.
(attached as whole files, since the patch would be unreadable)

The main point of the rewrite is splitting of a large part of the code
into Python. This should have the following advantages:
1) python-notmuch bindings can be used, which should allow for cleaner
   and more reliable code than running the binary and parsing its output
   with regexps.
   (also provides a nice use case for python-notmuch)
2) Python's huge standard library makes implementing some features MUCH easier.
3) More people know Python than vimscript, thus making the client
   development easier

The code is α quality, but should be close to usable.
It already has some features not present in the mainline vim client,
like attachments (viewing and sending, saving to file should be trivial
to add, will be done when I have some time), better support for unicode
and more.

Some UI features from the mainline versions that I didn't use were
removed and customization options are somewhat lacking atm. This is of
course to be improved later, depending on the responses.

Comments, bugreports and fixes very much welcome.

--
Anton Khirnov
" notmuch.vim plugin --- run notmuch within vim
"
" Copyright © Carl Worth
"
" This file is part of Notmuch.
"
" Notmuch is free software: you can redistribute it and/or modify it
" under the terms of the GNU General Public License as published by
" the Free Software Foundation, either version 3 of the License, or
" (at your option) any later version.
"
" Notmuch is distributed in the hope that it will be useful, but
" WITHOUT ANY WARRANTY; without even the implied warranty of
" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
" General Public License for more details.
"
" You should have received a copy of the GNU General Public License
" along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
"
" Authors: Bart Trojanowski <bart@jukie.net>
" Contributors: Felipe Contreras <felipe.contreras@gmail.com>,
"   Peter Hartman <peterjohnhartman@gmail.com>
"

if exists('s:notmuch_loaded') || &cp
    finish
endif
let s:notmuch_loaded = 1


" --- configuration defaults {{{1

let s:notmuch_defaults = {
        \ 'g:notmuch_cmd':                           'notmuch'                    ,
        \
        \ 'g:notmuch_search_newest_first':           1                            ,
        \
        \ 'g:notmuch_compose_insert_mode_start':     1                            ,
        \ 'g:notmuch_compose_header_help':           1                            ,
        \ 'g:notmuch_compose_temp_file_dir':         '~/.notmuch/compose/'        ,
        \ 'g:notmuch_fcc_maildir':                   'sent'                       ,
        \ }

" 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'               ],
        \ ]

let s:notmuch_show_headers_defaults = [
    \ 'From',
    \ 'To',
    \ 'Cc',
    \ 'Subject',
    \ 'Date',
    \ 'Reply-To',
    \ 'Message-Id',
    \]

" 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 <SID>NM_new_mail()<CR>',
        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',
        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',
        \ '=':          ':call <SID>NM_folders_refresh_view()<CR>',
        \ '<Enter>':    ':call <SID>NM_folders_show_search('''')<CR>',
        \ '<Space>':    ':call <SID>NM_folders_show_search(''tag:unread'')<CR>',
        \ 'tt':         ':call <SID>NM_folders_from_tags()<CR>',
        \ }

" --- --- bindings for search screen {{{2
let g:notmuch_search_maps = {
        \ '<Space>':    ':call <SID>NM_search_show_thread()<CR>',
        \ '<Enter>':    ':call <SID>NM_search_show_thread()<CR>',
        \ '<C-]>':      ':call <SID>NM_search_expand(''<cword>'')<CR>',
        \ 'a':          ':call <SID>NM_search_archive_thread()<CR>',
        \ 'A':          ':call <SID>NM_search_mark_read_then_archive_thread()<CR>',
        \ 'D':          ':call <SID>NM_search_delete_thread()<CR>',
        \ 'f':          ':call <SID>NM_search_filter()<CR>',
        \ 'm':          ':call <SID>NM_new_mail()<CR>',
        \ 'o':          ':call <SID>NM_search_toggle_order()<CR>',
        \ 'r':          ':call <SID>NM_search_reply_to_thread()<CR>',
        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',
        \ ',s':         ':call <SID>NM_search_prompt(1)<CR>',
        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',
        \ '+':          ':call <SID>NM_search_add_tags([])<CR>',
        \ '-':          ':call <SID>NM_search_remove_tags([])<CR>',
        \ '=':          ':call <SID>NM_search_refresh_view()<CR>',
        \ }

" --- --- bindings for show screen {{{2
let g:notmuch_show_maps = {
        \ '<C-P>':      ':call <SID>NM_jump_message(-1)<CR>',
        \ '<C-N>':      ':call <SID>NM_jump_message(+1)<CR>',
        \ '<C-]>':      ':call <SID>NM_search_expand(''<cword>'')<CR>',
        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',
        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',
        \
        \
        \ 'a':          ':call <SID>NM_show_archive_thread()<CR>',
        \ 'A':          ':call <SID>NM_show_mark_read_then_archive_thread()<CR>',
        \ 'N':          ':call <SID>NM_show_mark_read_then_next_open_message()<CR>',
        \ 'v':          ':call <SID>NM_show_view_all_mime_parts()<CR>',
        \ '+':          ':call <SID>NM_show_add_tag()<CR>',
        \ '-':          ':call <SID>NM_show_remove_tag()<CR>',
        \ '<Space>':    ':call <SID>NM_show_advance()<CR>',
        \ '\|':         ':call <SID>NM_show_pipe_message()<CR>',
        \
        \ '<S-Tab>':    ':call <SID>NM_show_previous_fold()<CR>',
        \ '<Tab>':      ':call <SID>NM_show_next_fold()<CR>',
        \ '<Enter>':    ':call <SID>NM_show_view_attachment()<CR>',
        \
        \ 'r':          ':call <SID>NM_show_reply()<CR>',
        \ 'm':          ':call <SID>NM_new_mail()<CR>',
        \ }

" --- --- bindings for compose screen {{{2
let g:notmuch_compose_nmaps = {
        \ ',s':         ':call <SID>NM_compose_send()<CR>',
        \ ',a':         ':call <SID>NM_compose_attach()<CR>',
        \ ',q':         ':call <SID>NM_kill_this_buffer()<CR>',
        \ '<Tab>':      ':call <SID>NM_compose_next_entry_area()<CR>',
        \ }
let g:notmuch_compose_imaps = {
        \ '<Tab>':      '<C-r>=<SID>NM_compose_next_entry_area()<CR>',
        \ }

" --- implement folders screen {{{1

" Create the folders buffer.
" Takes a list of [ folder name, query string]
" TODO decorate (help on the first line?)
function! s:NM_cmd_folders(folders)
    call <SID>NM_create_buffer('folders')
    silent 0put!='    Notmuch plugin.'
    python nm_vim.SavedSearches(vim.eval("a:folders"))
    call <SID>NM_finalize_menu_buffer()
    call <SID>NM_set_map('n', g:notmuch_folders_maps)
endfunction

" Show a folder for each existing tag.
function! s:NM_folders_from_tags()
    let folders = []
    python nm_vim.vim_get_tags()
    for tag in split(taglist, '\n')
        call add(folders, [tag, 'tag:' . tag ])
    endfor

    call <SID>NM_cmd_folders(folders)
endfunction

" --- --- folders screen action functions {{{2

" Refresh the folders screen
function! s:NM_folders_refresh_view()
        let lno = line('.')
        setlocal modifiable
        silent norm 3GdG
        python nm_vim.get_current_buffer().refresh()
        setlocal nomodifiable
        exec printf('norm %dG', lno)
endfunction

" Show contents of the folder corresponding to current line AND query
function! s:NM_folders_show_search(query)
    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
    if exists('obj')
        if len(a:query)
            let querystr = '(' . obj['id'] . ') and ' . a:query
        else
            let querystr = obj['id']
        endif

        call <SID>NM_cmd_search(querystr, 0)
    endif
endfunction

" Create the search buffer corresponding to querystr.
" If relative is 1, the search is relative to current buffer
function! s:NM_cmd_search(querystr, relative)
    let cur_buf = bufnr('%')
    call <SID>NM_create_buffer('search')
    if a:relative
        exec printf('python nm_vim.Search(querystr = "%s", parent = nm_vim.nm_buffers["%d"])', a:querystr, cur_buf)
    else
        exec printf('python nm_vim.Search(querystr = "%s")', a:querystr)
    endif
    call <SID>NM_finalize_menu_buffer()
    call <SID>NM_set_map('n', g:notmuch_search_maps)
endfunction

" --- --- search screen action functions {{{2

" Show the thread corresponding to current line
function! s:NM_search_show_thread()
    let querystr = <SID>NM_search_thread_id()
    if len(querystr)
        call <SID>NM_cmd_show(querystr)
    endif
endfunction

" Search according to input from user.
" If edit is 1, current query string is inserted to prompt for editing.
function! s:NM_search_prompt(edit)
    if a:edit
        python nm_vim.vim_get_id()
    else
        let buf_id = ''
    endif
    let querystr = input('Search: ', buf_id, 'custom,Search_type_completion')
    if len(querystr)
        call <SID>NM_cmd_search(querystr, 0)
    endif
endfunction

" Filter current search, i.e. search for
" (current querystr) AND (user input)
function! s:NM_search_filter()
    let querystr = input('Filter: ', '', 'custom,Search_type_completion')
    if len(querystr)
        call <SID>NM_cmd_search(querystr, 1)
    endif
endfunction

""""""""""""""""""""""'' TODO
function! s:NM_search_archive_thread()
        call <SID>NM_tag([], ['-inbox'])
        norm j
endfunction

function! s:NM_search_mark_read_then_archive_thread()
        call <SID>NM_tag([], ['-unread', '-inbox'])
        norm j
endfunction

function! s:NM_search_delete_thread()
        call <SID>NM_tag([], ['+junk','-inbox','-unread'])
        norm j
endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""

" XXX This function is broken
function! s:NM_search_toggle_order()
        let g:notmuch_search_newest_first = !g:notmuch_search_newest_first
        " FIXME: maybe this would be better done w/o reading re-reading the lines
        "         reversing the b:nm_raw_lines and the buffer lines would be better
        call <SID>NM_search_refresh_view()
endfunction

"XXX this function is broken
function! s:NM_search_reply_to_thread()
    python vim.command('let querystr = "%s"'%nm_vim.get_current_buffer().id)
    let cmd = ['reply']
    call add(cmd, <SID>NM_search_thread_id())
    call add(cmd, 'AND')
    call extend(cmd, [querystr])

    let data = <SID>NM_run(cmd)
    let lines = split(data, "\n")
    call <SID>NM_newComposeBuffer(lines, 0)
endfunction

function! s:NM_search_add_tags(tags)
        call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
endfunction

function! s:NM_search_remove_tags(tags)
        call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
endfunction

function! s:NM_search_refresh_view()
        let lno = line('.')
        setlocal modifiable
        norm ggdG
        python nm_vim.get_current_buffer().refresh()
        setlocal nomodifiable
        " 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_thread_id()
    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
    if exists('obj')
        return 'thread:' . obj['id']
    endif
    return ''
endfunction

function! s:NM_search_add_remove_tags(prompt, prefix, intags)
        if type(a:intags) != type([]) || len(a:intags) == 0
                " TODO: input() can support completion
                let text = input(a:prompt)
                if !strlen(text)
                        return
                endif
                let tags = split(text, ' ')
        else
                let tags = a:intags
        endif
        call map(tags, 'a:prefix . v:val')
        call <SID>NM_tag([], tags)
endfunction

" --- implement show screen {{{1

function! s:NM_cmd_show(querystr)
    "TODO: folding, syntax
    call <SID>NM_create_buffer('show')
    exec printf('python nm_vim.ShowThread("%s")', a:querystr)

    call <SID>NM_set_map('n', g:notmuch_show_maps)
    setlocal fillchars=
    setlocal foldtext=NM_show_foldtext()
    setlocal foldcolumn=6
    setlocal foldmethod=syntax
endfunction

function! s:NM_jump_message(offset)
    "TODO implement can_change_thread and find_matching, nicer positioning
    exec printf('python nm_vim.vim_get_object(%d, %d)', line('.'), a:offset)
    if exists('obj')
        silent norm zc
        exec printf('norm %dGzt', obj['start'])
        silent norm zo
    endif
endfunction

function! s:NM_show_next_thread()
        call <SID>NM_kill_this_buffer()
        if line('.') != line('$')
                norm j
                call <SID>NM_search_show_thread()
        else
                echo 'No more messages.'
        endif
endfunction

function! s:NM_show_archive_thread()
        call <SID>NM_tag('', ['-inbox'])
        call <SID>NM_show_next_thread()
endfunction

function! s:NM_show_mark_read_then_archive_thread()
        call <SID>NM_tag('', ['-unread', '-inbox'])
        call <SID>NM_show_next_thread()
endfunction

function! s:NM_show_mark_read_then_next_open_message()
        echo 'not implemented'
endfunction

function! s:NM_show_previous_message()
        echo 'not implemented'
endfunction

"XXX pythonise
function! s:NM_show_reply()
    let cmd = ['reply']
    call add(cmd, 'id:' . <SID>NM_show_message_id())

    let data = <SID>NM_run(cmd)
    let lines = split(data, "\n")
    call <SID>NM_newComposeBuffer(lines, 0)
endfunction

function! s:NM_show_view_all_mime_parts()
        echo 'not implemented'
endfunction

function! s:NM_show_view_raw_message()
        echo 'not implemented'
endfunction

function! s:NM_show_add_tag()
        echo 'not implemented'
endfunction

function! s:NM_show_remove_tag()
        echo 'not implemented'
endfunction

function! s:NM_show_advance()
    let advance_tags = ['-unread']

    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
    if !exists('obj')
        return
    endif

    call <SID>NM_tag(['id:' . obj['id']], advance_tags)
    if obj['end'] == line('$')
        call <SID>NM_kill_this_buffer()
    else
        call <SID>NM_jump_message(1)
    endif
endfunction

function! s:NM_show_pipe_message()
        echo 'not implemented'
endfunction

function! s:NM_show_view_attachment()
    exec printf('python nm_vim.vim_view_attachment(%d)', line('.'))
endfunction

" --- --- show screen helper functions {{{2

function! s:NM_show_message_id()
    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
    if exists('obj')
        return obj['id']
    else
        return ''
endfunction

" --- implement compose screen {{{1

function! s:NM_cmd_compose(words, body_lines)
        let lines = []
        let start_on_line = 0

        let hdrs = { }

        if !has_key(hdrs, 'From') || !len(hdrs['From'])
                let me = <SID>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

        call extend(lines, [ '', '' ])

        call <SID>NM_newComposeBuffer(lines, start_on_line)
endfunction

function! s:NM_compose_send()
    let fname = expand('%')

    try
        python nm_vim.get_current_buffer().send()
        call <SID>NM_kill_this_buffer()

        call delete(fname)
        echo 'Mail sent successfully.'
    endtry
endfunction

function! s:NM_compose_attach()
    let attachment = input('Enter attachment filename: ', '', 'file')
    if len(attachment)
        exec printf('python nm_vim.get_current_buffer().attach("%s")', attachment)
    endif
endfunction

function! s:NM_compose_next_entry_area()
        let lnum = line('.')
        let hdr_end = <SID>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_create_buffer(type)
    let prev_bufnr = bufnr('%')

    enew
    setlocal buftype=nofile
    execute printf('set filetype=notmuch-%s', a:type)
    execute printf('set syntax=notmuch-%s', a:type)
    "XXX this should probably go
    let b:nm_prev_bufnr = prev_bufnr
endfunction

"set some options for "menu"-like buffers -- folders/searches
function! s:NM_finalize_menu_buffer()
    setlocal nomodifiable
    setlocal cursorline
    setlocal nowrap
endfunction

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:type)
        execute printf('set syntax=notmuch-%s', 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 = <SID>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)
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:   <Tab> - 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
        if exists('g:notmuch_signature')
                call extend(lines, ['', '--'])
                call extend(lines, g:notmuch_signature)
        endif


        let prev_bufnr = bufnr('%')
        call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail',
                                  \ 'compose', lines)
        let b:nm_prev_bufnr = prev_bufnr

        call <SID>NM_set_map('n', g:notmuch_compose_nmaps)
        call <SID>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 <SID>NM_compose_next_entry_area()
        endif

        if g:notmuch_compose_insert_mode_start
                startinsert!
        endif

        python nm_vim.Compose()
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

function! s:NM_run(args)
    let words = a:args
    call map(words, 's:NM_shell_escape(v:val)')
    let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null'

    let out = system(cmd)
    let err = v:shell_error

    if err
        echohl Error
        echo substitute(out, '\n*$', '', '')
        echohl None
        return ''
    else
        return out
    endif
endfunction

" --- external mail handling helpers {{{1

function! s:NM_new_mail()
        call <SID>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_kill_this_buffer()
    let prev_bufnr = b:nm_prev_bufnr
    python nm_vim.delete_current_buffer()
    bdelete!
    exec printf("buffer %d", prev_bufnr)
endfunction

function! s:NM_search_expand(arg)
        let word = expand(a:arg)
        let prev_bufnr = bufnr('%')
        call <SID>NM_cmd_search(word, 0)
        let b:nm_prev_bufnr = prev_bufnr
endfunction

function! s:NM_tag(filter, tags)
    let filter = len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]
    if !len(filter)
        throw 'Eeek! I couldn''t find the thead id!'
    endif
    exec printf('python nm_vim.get_current_buffer().tag(tags = vim.eval("a:tags"), querystr = "%s")', join(filter))
endfunction

" --- process and set the defaults {{{1

function! NM_set_defaults(force)
    setlocal bufhidden=hide
    for [key, dflt] in items(s:notmuch_defaults)
        let cmd = ''
        if !a:force && exists(key) && type(dflt) == type(eval(key))
            continue
        elseif type(dflt) == type(0)
            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
            echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]',
                        \ a:force, key, string(dflt))
            continue
        endif
        exec cmd
    endfor
endfunction
call NM_set_defaults(0)

" for some reason NM_set_defaults() didn't work for arrays...
if !exists('g:notmuch_folders')
    let g:notmuch_folders = s:notmuch_folders_defaults
endif

if !exists('g:notmuch_show_headers')
    let g:notmuch_show_headers = s:notmuch_show_headers_defaults
endif

if !exists('g:notmuch_signature')
        if filereadable(glob('~/.signature'))
            let g:notmuch_signature = readfile(glob('~/.signature'))
        endif
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(type, maps)
        for [key, code] in items(a:maps)
                exec printf('%snoremap <buffer> %s %s', a:type, key, code)
        endfor
endfunction

" --- command handler {{{1

function! NotMuch()
    if !exists('s:notmuch_inited')
        " init the python layer
        python import sys
        exec "python sys.path += [r'" . s:python_path . "']"
        python import vim, nm_vim

        let s:notmuch_inited = 1
    endif

    call <SID>NM_cmd_folders(g:notmuch_folders)
endfunction

"Custom foldtext() for show buffers, which indents folds to
"represent thread structure
function! NM_show_foldtext()
    if v:foldlevel != 1
        return foldtext()
    endif
    let numlines = v:foldend - v:foldstart + 1
    let indentlevel = matchstr(getline(v:foldstart), '^[0-9]\+')
    return repeat('  ', indentlevel) . getline(v:foldstart + 1)
endfunction

"Completion of search prompt
"TODO properly deal with complex queries
function! Search_type_completion(arg_lead, cmd_line, cursor_pos)
    let idx = stridx(a:arg_lead, ':')
    if idx < 0
        return 'from:' .       "\n" .
             \ 'to:' .         "\n" .
             \ 'subject:' .    "\n" .
             \ 'attachment:' . "\n" .
             \ 'tag:' .        "\n" .
             \ 'id:' .         "\n" .
             \ 'thread:' .     "\n" .
             \ 'folder:'
    endif
    if stridx(a:arg_lead, 'tag:') >= 0
        python nm_vim.vim_get_tags()
        return 'tag:' . substitute(taglist, "\n", "\ntag:", "g")
    endif
    return ''
endfunction

" --- glue {{{1

command! NotMuch call NotMuch()
let s:python_path = expand('<sfile>:p:h')
" notmuch folders mode syntax file

syntax region nmFolfers             start=/^/ end=/$/         oneline contains=nmFoldersMessageCount
syntax match  nmFoldersMessageCount /^ *[0-9]\+ */            contained nextgroup=nmFoldersUnreadCount
syntax match  nmFoldersUnreadCount  /(.\{-}) */               contained nextgroup=nmFoldersName
syntax match  nmFoldersName         /.*\ze(/                  contained nextgroup=nmFoldersSearch
syntax match  nmFoldersSearch       /([^()]\+)$/

highlight link nmFoldersMessageCount Statement
highlight link nmFoldersUnreadCount  Underlined
highlight link nmFoldersName         Type
highlight link nmFoldersSearch       String

highlight CursorLine term=reverse cterm=reverse gui=reverse

syntax region nmSearch          start=/^/ end=/$/       oneline contains=nmSearchDate keepend
syntax match nmSearchDate       /^.\{-13}/              contained nextgroup=nmSearchNum skipwhite
syntax match nmSearchNum        "[0-9]\+\/"             contained nextgroup=nmSearchTotal skipwhite
syntax match nmSearchTotal      /[0-9]\+/               contained nextgroup=nmSearchFrom skipwhite
syntax match nmSearchFrom       /.\{-}\ze|/             contained nextgroup=nmSearchSubject skipwhite
"XXX this fails on some messages with multiple authors
syntax match nmSearchSubject    /.*\ze(/                contained nextgroup=nmSearchTags
syntax match nmSearchTags       /.\+$/                  contained

syntax match nmUnread           /^.*(.*\<unread\>.*)$/

highlight link nmSearchDate    Statement
highlight link nmSearchNum     Number
highlight link nmSearchTotal   Type
highlight link nmSearchFrom    Include
highlight link nmSearchSubject Normal
highlight link nmSearchTags    String

highlight link nmUnread        Underlined
" notmuch show mode syntax file

setlocal conceallevel=2
setlocal concealcursor=vinc

syntax region  nmMessage     matchgroup=Ignore concealends start='[0-9]\+\/-*message start-*\\' end='\\-*message end-*\/' fold contains=@nmShowMsgBody keepend

"TODO what about those
syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags
syntax match   nmShowMsgDescWho /[^)]\+)/ contained
syntax match   nmShowMsgDescDate / ([^)]\+[0-9]) / contained
syntax match   nmShowMsgDescTags /([^)]\+)$/ contained

syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit
syntax include @nmShowMsgBodyMail syntax/mail.vim
silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim

highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse
highlight link nmShowMsgDescDate Type
highlight link nmShowMsgDescTags String

"TODO what about this?
highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black

Thread: