notmuch/vim/notmuch.vim
Felipe Contreras 85b3dc9f0f vim: refactor database handling
To minimize memory usage we need to destroy the queries and the
databases, so we should keep track of them.

Each buffer gets a database connection that is destroyed when the buffer
is destroyed, and all the queries along with it.

Ideally notmuch should destroy the queries when the database is
destroyed, but it's not doing that at the moment.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
2013-11-20 07:23:29 -06:00

854 lines
17 KiB
VimL

if exists("g:loaded_notmuch_rb")
finish
endif
if !has("ruby") || version < 700
finish
endif
let g:loaded_notmuch_rb = "yep"
let g:notmuch_rb_folders_maps = {
\ '<Enter>': 'folders_show_search()',
\ 's': 'folders_search_prompt()',
\ '=': 'folders_refresh()',
\ }
let g:notmuch_rb_search_maps = {
\ 'q': 'kill_this_buffer()',
\ '<Enter>': 'search_show_thread(1)',
\ '<Space>': 'search_show_thread(2)',
\ 'A': 'search_tag("-inbox -unread")',
\ 'I': 'search_tag("-unread")',
\ 't': 'search_tag("")',
\ 's': 'search_search_prompt()',
\ '=': 'search_refresh()',
\ '?': 'search_info()',
\ }
let g:notmuch_rb_show_maps = {
\ 'q': 'kill_this_buffer()',
\ 'A': 'show_tag("-inbox -unread")',
\ 'I': 'show_tag("-unread")',
\ 't': 'show_tag("")',
\ 'o': 'show_open_msg()',
\ 'e': 'show_extract_msg()',
\ 's': 'show_save_msg()',
\ 'r': 'show_reply()',
\ '?': 'show_info()',
\ '<Tab>': 'show_next_msg()',
\ }
let g:notmuch_rb_compose_maps = {
\ ',s': 'compose_send()',
\ ',q': 'compose_quit()',
\ }
let s:notmuch_rb_folders_default = [
\ [ 'new', 'tag:inbox and tag:unread' ],
\ [ 'inbox', 'tag:inbox' ],
\ [ 'unread', 'tag:unread' ],
\ ]
let s:notmuch_rb_date_format_default = '%d.%m.%y'
let s:notmuch_rb_datetime_format_default = '%d.%m.%y %H:%M:%S'
let s:notmuch_rb_reader_default = 'mutt -f %s'
let s:notmuch_rb_sendmail_default = 'sendmail'
let s:notmuch_rb_folders_count_threads_default = 0
if !exists('g:notmuch_rb_date_format')
let g:notmuch_rb_date_format = s:notmuch_rb_date_format_default
endif
if !exists('g:notmuch_rb_datetime_format')
let g:notmuch_rb_datetime_format = s:notmuch_rb_datetime_format_default
endif
if !exists('g:notmuch_rb_reader')
let g:notmuch_rb_reader = s:notmuch_rb_reader_default
endif
if !exists('g:notmuch_rb_sendmail')
let g:notmuch_rb_sendmail = s:notmuch_rb_sendmail_default
endif
if !exists('g:notmuch_rb_folders_count_threads')
let g:notmuch_rb_folders_count_threads = s:notmuch_rb_folders_count_threads_default
endif
function! s:new_file_buffer(type, fname)
exec printf('edit %s', a:fname)
execute printf('set filetype=notmuch-%s', a:type)
execute printf('set syntax=notmuch-%s', a:type)
ruby $curbuf.init(VIM::evaluate('a:type'))
ruby $buf_queue.push($curbuf.number)
endfunction
function! s:compose_unload()
if b:compose_done
return
endif
if input('[s]end/[q]uit? ') =~ '^s'
call s:compose_send()
endif
endfunction
"" actions
function! s:compose_quit()
let b:compose_done = 1
call s:kill_this_buffer()
endfunction
function! s:compose_send()
let b:compose_done = 1
let fname = expand('%')
" remove headers
0,4d
write
let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname
let out = system(cmdtxt)
let err = v:shell_error
if err
undo
write
echohl Error
echo 'Eeek! unable to send mail'
echo out
echohl None
return
endif
call delete(fname)
echo 'Mail sent successfully.'
call s:kill_this_buffer()
endfunction
function! s:show_next_msg()
ruby << EOF
r, c = $curwin.cursor
n = $curbuf.line_number
i = $messages.index { |m| n >= m.start && n <= m.end }
m = $messages[i + 1]
if m
r = m.body_start + 1
VIM::command("normal #{m.start}zt")
$curwin.cursor = r, c
end
EOF
endfunction
function! s:show_reply()
ruby open_reply get_message.mail
let b:compose_done = 0
call s:set_map(g:notmuch_rb_compose_maps)
autocmd BufUnload <buffer> call s:compose_unload()
startinsert!
endfunction
function! s:show_info()
ruby vim_puts get_message.inspect
endfunction
function! s:show_extract_msg()
ruby << EOF
m = get_message
m.mail.attachments.each do |a|
File.open(a.filename, 'w') do |f|
f.write a.body.decoded
print "Extracted '#{a.filename}'"
end
end
EOF
endfunction
function! s:show_open_msg()
ruby << EOF
m = get_message
mbox = File.expand_path('~/.notmuch/vim_mbox')
cmd = VIM::evaluate('g:notmuch_rb_reader') % mbox
system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}"
EOF
endfunction
function! s:show_save_msg()
let file = input('File name: ')
ruby << EOF
file = VIM::evaluate('file')
m = get_message
system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
EOF
endfunction
function! s:show_tag(intags)
if empty(a:intags)
let tags = input('tags: ')
else
let tags = a:intags
endif
ruby do_tag(get_cur_view, VIM::evaluate('l:tags'))
call s:show_next_thread()
endfunction
function! s:search_search_prompt()
let text = input('Search: ')
setlocal modifiable
ruby << EOF
$cur_search = VIM::evaluate('text')
$curbuf.reopen
search_render($cur_search)
EOF
setlocal nomodifiable
endfunction
function! s:search_info()
ruby vim_puts get_thread_id
endfunction
function! s:search_refresh()
setlocal modifiable
ruby $curbuf.reopen
ruby search_render($cur_search)
setlocal nomodifiable
endfunction
function! s:search_tag(intags)
if empty(a:intags)
let tags = input('tags: ')
else
let tags = a:intags
endif
ruby do_tag(get_thread_id, VIM::evaluate('l:tags'))
norm j
endfunction
function! s:folders_search_prompt()
let text = input('Search: ')
call s:search(text)
endfunction
function! s:folders_refresh()
setlocal modifiable
ruby $curbuf.reopen
ruby folders_render()
setlocal nomodifiable
endfunction
"" basic
function! s:show_cursor_moved()
ruby << EOF
if $render.is_ready?
VIM::command('setlocal modifiable')
$render.do_next
VIM::command('setlocal nomodifiable')
end
EOF
endfunction
function! s:show_next_thread()
call s:kill_this_buffer()
if line('.') != line('$')
norm j
call s:search_show_thread(0)
else
echo 'No more messages.'
endif
endfunction
function! s:kill_this_buffer()
ruby $curbuf.close
bdelete!
ruby << EOF
$buf_queue.pop
b = $buf_queue.last
VIM::command("buffer #{b}") if b
EOF
endfunction
function! s:set_map(maps)
nmapclear <buffer>
for [key, code] in items(a:maps)
let cmd = printf(":call <SID>%s<CR>", code)
exec printf('nnoremap <buffer> %s %s', key, cmd)
endfor
endfunction
function! s:new_buffer(type)
enew
setlocal buftype=nofile bufhidden=hide
keepjumps 0d
execute printf('set filetype=notmuch-%s', a:type)
execute printf('set syntax=notmuch-%s', a:type)
ruby $curbuf.init(VIM::evaluate('a:type'))
ruby $buf_queue.push($curbuf.number)
endfunction
function! s:set_menu_buffer()
setlocal nomodifiable
setlocal cursorline
setlocal nowrap
endfunction
"" main
function! s:show(thread_id)
call s:new_buffer('show')
setlocal modifiable
ruby << EOF
thread_id = VIM::evaluate('a:thread_id')
$cur_thread = thread_id
$messages.clear
$curbuf.render do |b|
q = $curbuf.query(get_cur_view)
q.sort = 0
msgs = q.search_messages
msgs.each do |msg|
m = Mail.read(msg.filename)
part = m.find_first_text
nm_m = Message.new(msg, m)
$messages << nm_m
date_fmt = VIM::evaluate('g:notmuch_rb_datetime_format')
date = Time.at(msg.date).strftime(date_fmt)
nm_m.start = b.count
b << "%s %s (%s)" % [msg['from'], date, msg.tags]
b << "Subject: %s" % [msg['subject']]
b << "To: %s" % msg['to']
b << "Cc: %s" % msg['cc']
b << "Date: %s" % msg['date']
nm_m.body_start = b.count
b << "--- %s ---" % part.mime_type
part.convert.each_line do |l|
b << l.chomp
end
b << ""
nm_m.end = b.count
end
b.delete(b.count)
end
$messages.each_with_index do |msg, i|
VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1])
VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start])
VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end])
end
EOF
setlocal nomodifiable
call s:set_map(g:notmuch_rb_show_maps)
endfunction
function! s:search_show_thread(mode)
ruby << EOF
mode = VIM::evaluate('a:mode')
id = get_thread_id
case mode
when 0;
when 1; $cur_filter = nil
when 2; $cur_filter = $cur_search
end
VIM::command("call s:show('#{id}')")
EOF
endfunction
function! s:search(search)
call s:new_buffer('search')
ruby << EOF
$cur_search = VIM::evaluate('a:search')
search_render($cur_search)
EOF
call s:set_menu_buffer()
call s:set_map(g:notmuch_rb_search_maps)
autocmd CursorMoved <buffer> call s:show_cursor_moved()
endfunction
function! s:folders_show_search()
ruby << EOF
n = $curbuf.line_number
s = $searches[n - 1]
VIM::command("call s:search('#{s}')")
EOF
endfunction
function! s:folders()
call s:new_buffer('folders')
ruby folders_render()
call s:set_menu_buffer()
call s:set_map(g:notmuch_rb_folders_maps)
endfunction
"" root
function! s:set_defaults()
if exists('g:notmuch_rb_custom_search_maps')
call extend(g:notmuch_rb_search_maps, g:notmuch_rb_custom_search_maps)
endif
if exists('g:notmuch_rb_custom_show_maps')
call extend(g:notmuch_rb_show_maps, g:notmuch_rb_custom_show_maps)
endif
" TODO for now lets check the old folders too
if !exists('g:notmuch_rb_folders')
if exists('g:notmuch_folders')
let g:notmuch_rb_folders = g:notmuch_folders
else
let g:notmuch_rb_folders = s:notmuch_rb_folders_default
endif
endif
endfunction
function! s:NotMuch()
call s:set_defaults()
ruby << EOF
require 'notmuch'
require 'rubygems'
require 'tempfile'
begin
require 'mail'
rescue LoadError
end
$db_name = nil
$email_address = nil
$searches = []
$buf_queue = []
$threads = []
$messages = []
$config = {}
$mail_installed = defined?(Mail)
def get_config
group = nil
config = ENV['NOTMUCH_CONFIG'] || '~/.notmuch-config'
File.open(File.expand_path(config)).each do |l|
l.chomp!
case l
when /^\[(.*)\]$/
group = $1
when ''
when /^(.*)=(.*)$/
key = "%s.%s" % [group, $1]
value = $2
$config[key] = value
end
end
$db_name = $config['database.path']
$email_address = "%s <%s>" % [$config['user.name'], $config['user.primary_email']]
end
def vim_puts(s)
VIM::command("echo '#{s.to_s}'")
end
def vim_p(s)
VIM::command("echo '#{s.inspect}'")
end
def author_filter(a)
# TODO email format, aliases
a.strip!
a.gsub!(/[\.@].*/, '')
a.gsub!(/^ext /, '')
a.gsub!(/ \(.*\)/, '')
a
end
def get_thread_id
n = $curbuf.line_number - 1
return "thread:%s" % $threads[n]
end
def get_message
n = $curbuf.line_number
return $messages.find { |m| n >= m.start && n <= m.end }
end
def get_cur_view
if $cur_filter
return "#{$cur_thread} and (#{$cur_filter})"
else
return $cur_thread
end
end
def open_reply(orig)
help_lines = [
'Notmuch-Help: Type in your message here; to help you use these bindings:',
'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)',
'Notmuch-Help: ,q - abort the message',
]
reply = orig.reply do |m|
# fix headers
if not m[:reply_to]
m.to = [orig[:from].to_s, orig[:to].to_s]
end
m.cc = orig[:cc]
m.from = $email_address
m.charset = 'utf-8'
m.content_transfer_encoding = '7bit'
end
dir = File.expand_path('~/.notmuch/compose')
FileUtils.mkdir_p(dir)
Tempfile.open(['nm-', '.mail'], dir) do |f|
lines = []
lines += help_lines
lines << ''
body_lines = []
if $mail_installed
addr = Mail::Address.new(orig[:from].value)
name = addr.name
name = addr.local + "@" if name.nil? && !addr.local.nil?
else
name = orig[:from]
end
name = "somebody" if name.nil?
body_lines << "%s wrote:" % name
part = orig.find_first_text
part.convert.each_line do |l|
body_lines << "> %s" % l.chomp
end
body_lines << ""
body_lines << ""
body_lines << ""
reply.body = body_lines.join("\n")
lines += reply.to_s.lines.map { |e| e.chomp }
lines << ""
old_count = lines.count - 1
f.puts(lines)
sig_file = File.expand_path('~/.signature')
if File.exists?(sig_file)
f.puts("-- ")
f.write(File.read(sig_file))
end
f.flush
VIM::command("let s:reply_from='%s'" % reply.from.first.to_s)
VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
VIM::command("call cursor(#{old_count}, 0)")
end
end
def folders_render()
$curbuf.render do |b|
folders = VIM::evaluate('g:notmuch_rb_folders')
count_threads = VIM::evaluate('g:notmuch_rb_folders_count_threads')
$searches.clear
folders.each do |name, search|
q = $curbuf.query(search)
$searches << search
count = count_threads ? q.search_threads.count : q.search_messages.count
b << "%9d %-20s (%s)" % [count, name, search]
end
end
end
def search_render(search)
date_fmt = VIM::evaluate('g:notmuch_rb_date_format')
q = $curbuf.query(search)
$threads.clear
t = q.search_threads
$render = $curbuf.render_staged(t) do |b, items|
items.each do |e|
authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",")
date = Time.at(e.newest_date).strftime(date_fmt)
if $mail_installed
subject = Mail::Field.new("Subject: " + e.subject).to_s
else
subject = e.subject.force_encoding('utf-8')
end
b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags]
$threads << e.thread_id
end
end
end
def do_tag(filter, tags)
$curbuf.do_write do |db|
q = db.query(filter)
q.search_messages.each do |e|
e.freeze
tags.split.each do |t|
case t
when /^-(.*)/
e.remove_tag($1)
when /^\+(.*)/
e.add_tag($1)
when /^([^\+^-].*)/
e.add_tag($1)
end
end
e.thaw
e.tags_to_maildir_flags
end
q.destroy!
end
end
module DbHelper
def init(name)
@name = name
@db = Notmuch::Database.new($db_name)
@queries = []
end
def query(*args)
q = @db.query(*args)
@queries << q
q
end
def close
@queries.delete_if { |q| ! q.destroy! }
@db.close
end
def reopen
close if @db
@db = Notmuch::Database.new($db_name)
end
def do_write
db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
begin
yield db
ensure
db.close
end
end
end
class Message
attr_accessor :start, :body_start, :end
attr_reader :message_id, :filename, :mail
def initialize(msg, mail)
@message_id = msg.message_id
@filename = msg.filename
@mail = mail
@start = 0
@end = 0
mail.import_headers(msg) if not $mail_installed
end
def to_s
"id:%s" % @message_id
end
def inspect
"id:%s, file:%s" % [@message_id, @filename]
end
end
class StagedRender
def initialize(buffer, enumerable, block)
@b = buffer
@enumerable = enumerable
@block = block
@last_render = 0
@b.render { do_next }
end
def is_ready?
@last_render - @b.line_number <= $curwin.height
end
def do_next
items = @enumerable.take($curwin.height * 2)
return if items.empty?
@block.call @b, items
@last_render = @b.count
end
end
class VIM::Buffer
include DbHelper
def <<(a)
append(count(), a)
end
def render_staged(enumerable, &block)
StagedRender.new(self, enumerable, block)
end
def render
old_count = count
yield self
(1..old_count).each do
delete(1)
end
end
end
class Notmuch::Tags
def to_s
to_a.join(" ")
end
end
class Notmuch::Message
def to_s
"id:%s" % message_id
end
end
# workaround for bug in vim's ruby
class Object
def flush
end
end
module SimpleMessage
class Header < Array
def self.parse(string)
return nil if string.empty?
return Header.new(string.split(/,\s+/))
end
def to_s
self.join(', ')
end
end
def initialize(string = nil)
@raw_source = string
@body = nil
@headers = {}
return if not string
if string =~ /(.*?(\r\n|\n))\2/m
head, body = $1, $' || '', $2
else
head, body = string, ''
end
@body = body
end
def [](name)
@headers[name.to_sym]
end
def []=(name, value)
@headers[name.to_sym] = value
end
def format_header(value)
value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize }
end
def to_s
buffer = ''
@headers.each do |key, value|
buffer << "%s: %s\r\n" %
[format_header(key), value]
end
buffer << "\r\n"
buffer << @body
buffer
end
def body=(value)
@body = value
end
def from
@headers[:from]
end
def decoded
@body
end
def mime_type
'text/plain'
end
def multipart?
false
end
def reply
r = Mail::Message.new
r[:from] = self[:to]
r[:to] = self[:from]
r[:cc] = self[:cc]
r[:in_reply_to] = self[:message_id]
r[:references] = self[:references]
r
end
HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ]
def import_headers(m)
HEADERS.each do |e|
dashed = format_header(e)
@headers[e] = Header.parse(m[dashed])
end
end
end
module Mail
if not $mail_installed
puts "WARNING: Install the 'mail' gem, without it support is limited"
def self.read(filename)
Message.new(File.open(filename, 'rb') { |f| f.read })
end
class Message
include SimpleMessage
end
end
class Message
def find_first_text
return self if not multipart?
return text_part || html_part
end
def convert
if mime_type != "text/html"
text = decoded
else
IO.popen("elinks --dump", "w+") do |pipe|
pipe.write(decode_body)
pipe.close_write
text = pipe.read
end
end
text
end
end
end
class String
def to_utf8
RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self
end
end
get_config
EOF
call s:folders()
endfunction
command NotMuch :call s:NotMuch()
" vim: set noexpandtab: