From 6bc2f5a999d9ff77779d98b7cc688a4350ea65d9 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Mon, 9 May 2011 17:19:26 +0200 Subject: Add a python rewrite of the vim plugin. --- vim/plugin/nm_vim.py | 671 +++++++++++++++++++++++++++++++ vim/plugin/notmuch-vimpy.vim | 863 ++++++++++++++++++++++++++++++++++++++++ vim/syntax/nm_vimpy-compose.vim | 7 + vim/syntax/nm_vimpy-folders.vim | 15 + vim/syntax/nm_vimpy-search.vim | 18 + vim/syntax/nm_vimpy-show.vim | 23 ++ 6 files changed, 1597 insertions(+) create mode 100644 vim/plugin/nm_vim.py create mode 100644 vim/plugin/notmuch-vimpy.vim create mode 100644 vim/syntax/nm_vimpy-compose.vim create mode 100644 vim/syntax/nm_vimpy-folders.vim create mode 100644 vim/syntax/nm_vimpy-search.vim create mode 100644 vim/syntax/nm_vimpy-show.vim diff --git a/vim/plugin/nm_vim.py b/vim/plugin/nm_vim.py new file mode 100644 index 0000000..ef1dfb3 --- /dev/null +++ b/vim/plugin/nm_vim.py @@ -0,0 +1,671 @@ +# +# 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 . +# + +# python-notmuch wrapper for the notmuch vim client + +import codecs +import datetime +import email, email.header, email.charset, email.utils, email.message +import email.mime.text, email.mime.audio, email.mime.image, email.mime.multipart, email.mime.application +import mailbox +import mailcap +import mimetypes +import notmuch +import os, os.path +import shlex +import smtplib +import subprocess +import tempfile +import vim + +#### classes #### + +class NMBuffer(object): + """ + An object mapping line ranges in current buffer to notmuch structures. + """ + + # a string identifying the buffer + id = None + + # a list of NMBufferElement subclasses representing objects in current buffer + objects = None + + def __new__(cls, *args, **kwargs): + bufnr = vim.eval('bufnr("%")') + if bufnr in nm_buffers: + return nm_buffers[bufnr] + ret = object.__new__(cls) + nm_buffers[bufnr] = ret + return ret + + def __init__(self): + self.objects = [] + + def get_object(self, line, offset): + """ + Get an object that's offset objects away from given line or None. + E.g. offset = 0 gets the object on the line, offset = 1 gets + next, offset = -1 previous. + """ + if not self.objects: + return None + + if offset > 0 and line < self.objects[0].start: + return self.objects[min(len(self.objects), offset) - 1] + if offset < 0 and line > self.objects[-1].end: + return self.objects[-min(len(self.objects), offset)] + + for i in xrange(len(self.objects)): + obj = self.objects[i] + if line >= obj.start and line <= obj.end: + return self.objects[ max(min(len(self.objects) - 1, i + offset), 0) ] + + def tag(self, tags, querystr = None): + if querystr: + querystr = '( %s ) and ( %s )'%(self.id, querystr) + else: + querystr = self.id + + db = notmuch.Database(mode = notmuch.Database.MODE.READ_WRITE) + map_query(db, querystr, lambda m, l: self._tag_message(m, tags)) + + def _tag_message(self, message, tags): + for tag in tags: + if tag[0] == '+': + message.add_tag(tag[1:]) + elif tag[0] == '-': + message.remove_tag(tag[1:]) + +class SavedSearches(NMBuffer): + """ + This buffer displays a list of saved searches ('folders'). + """ + + def __init__(self, folders): + """ + @param folders A list of (folder name, query string) tuples. + """ + super(SavedSearches, self).__init__() + for folder in folders: + self.objects.append(Folder(0, 0, folder[1], folder[0])) + self.refresh() + + def refresh(self): + b = vim.current.buffer + db = notmuch.Database() + for obj in self.objects: + q = db.create_query(obj.id) + q1 = db.create_query('( %s ) and tag:unread'%(obj.id)) + b.append('{0:>7} {1:7} {2: <30s} ({3})'.format(q.count_messages(), '({0})'.format(q1.count_messages()), + obj.name, obj.id)) + obj.start = obj.end = len(b) - 1 + + def __repr__(self): + return '' + + def tag(self): + raise TypeError('Attempted to tag in folders view.') + +class Search(NMBuffer): + """ + This buffer displays results of a db search -- a list of found threads. + """ + + def __init__(self, querystr = '', parent = None): + super(Search, self).__init__() + + assert(querystr or parent) + + # FIXME simplify + if parent: + self.id = parent.id + if querystr: + if self.id: + self.id = '( %s ) and ( %s )'%(self.id, querystr) + else: + self.id = querystr + + self.refresh() + + def refresh(self): + self.objects = [] + b = vim.current.buffer + db = notmuch.Database() + q = db.create_query(self.id) + + q.set_sort(q.SORT.NEWEST_FIRST) # FIXME allow different search orders + for t in q.search_threads(): + start = len(b) + datestr = get_relative_date(t.get_newest_date()) + authors = t.get_authors() + + # notmuch returns different thread subjects depending on + # sort order + # we assume that 'thread subject' == 'subject of the first mail in the thread' + subj = None + for m in t.get_toplevel_messages(): + subj = m.get_header('subject') + break + if not subj: + subj = t.get_subject() + + tags = str(t.get_tags()) + b.append((u'%-12s %3s/%3s %-20.20s | %s (%s)'%(datestr, t.get_matched_messages(), t.get_total_messages(), + authors, subj, tags)).encode('utf-8')) + self.objects.append(NMBufferElement(start, len(b) - 1, t.get_thread_id())) + + def __repr__(self): + return ''%(self.id) + +class ShowThread(NMBuffer): + """ + This buffer represents a thread view. + """ + + # a list of temporary files for viewing attachments + # they will be automagically closed and deleted on this object's demise + _tmpfiles = None + + # a list of headers to show + _headers = None + + def __init__(self, querystr): + self.id = querystr + self._tmpfiles = [] + self.refresh() + + def refresh(self): + self._headers = vim.eval('g:nm_vimpy_show_headers') + self.objects = [] + db = notmuch.Database() + map_query(db, self.id, self._print_message) + + def _get_attachment(self, line): + message = self.get_object(line, 0) + if not message: + print 'No message on this line.' + return None + + data = None + for a in message.attachments: + if line == a.start: + return a.data + print 'No attachment on this line' + + def view_attachment(self, line): + """ + View attachment corresponding to given line. + """ + data = self._get_attachment(line) + if not data: + return + + f = tempfile.NamedTemporaryFile() + f.write(data.get_payload(decode = True)) + f.flush() + os.fsync(f.fileno()) + + caps = mailcap.getcaps() + ret = mailcap.findmatch(caps, data.get_content_type(), filename = f.name) + if ret[0]: + with open(os.devnull, 'w') as null: + subprocess.Popen(shlex.split(ret[0]), stderr = null, stdout = null) + self._tmpfiles.append(f) + + def save_attachment(self, line, filename): + """ + Save attachment corresponding to given line to the specified file. + """ + data = self._get_attachment(line) + if not data: + return + + if os.path.isdir(filename): + filename = os.path.join(filename, data.get_filename()) + + with open(os.path.expanduser(filename), 'w') as f: + f.write(data.get_payload(decode = True)) + + def _read_text_payload(self, part): + """ + Try converting the payload of the MIME part into utf-8. + """ + p = part.get_payload(decode = True) + ch = part.get_content_charset('utf-8') + try: + codec = codecs.lookup(ch) + except LookupError: + print 'Unknown encoding: %s'%ch + return p + + if codec.name != 'utf-8': + try: + p = p.decode(codec.name).encode('utf-8') + except UnicodeDecodeError: + return 'Error decoding the message.' + return p + + def _print_part(self, part): + """ + Walk through the part recursively and print it + and its subparts to current buffer. + """ + if part.is_multipart(): + if part.get_content_subtype() == 'alternative': + # try to find the plaintext version, if that fails just try to print the first + for subpart in part.get_payload(): + if subpart.get_content_type() == 'text/plain': + return self._print_part(subpart) + self._print_part(part.get_payload()[0]) + else: + for subpart in part.get_payload(): + self._print_part(subpart) + else: + b = vim.current.buffer + if part.get('Content-Disposition', '').lower().startswith('attachment'): + self.objects[-1].attachments.append(Attachment(len(b), len(b), part)) + b.append(('[ Attachment: %s (%s)]'%(part.get_filename(), part.get_content_type())).split('\n')) + + if part.get_content_maintype() == 'text': + p = self._read_text_payload(part) + b.append(p.split('\n')) + + def _print_message(self, message, level): + b = vim.current.buffer + msg = Message(len(b), 0, message.get_message_id()) + self.objects.append(msg) + + msg.tags = list(message.get_tags()) + + fp = open(message.get_filename()) + email_msg = email.message_from_file(fp) + fp.close() + + # print the title + b.append('%d/'%level + 20*'-' + 'message start' + 20*'-' + '\\') + + name, addr = email.utils.parseaddr(message.get_header('from')) + author = name if name else addr + titlestr = '%s: %s (%s) (%s)'%(author, message.get_header('subject'), get_relative_date(message.get_date()), + ' '.join(message.get_tags())) + b.append(titlestr.encode('utf-8')) + + # print the headers + for header in self._headers: + if header in email_msg: + b.append(('%s: %s'%(header, decode_header(email_msg[header]))).split('\n')) + b.append('') + + self._print_part(email_msg) + + b.append('\\' + 20*'-' + 'message end' + 20*'-' + '/') + msg.end = len(b) - 1 + + def __repr__(self): + return ''%self.id + + def get_message_for_tag(self, tag): + """ + Return the first message for given tag. + """ + for msg in self.objects: + if tag in msg.tags: + return msg + return None + +class Compose(NMBuffer): + + _attachment_prefix = 'Notmuch-Attachment: ' + + def __init__(self): + super(Compose, self).__init__() + # python wants to use base64 for some reason, force quoted-printable + email.charset.add_charset('utf-8', email.charset.QP, email.charset.QP, 'utf-8') + + def _encode_header(self, text): + try: + text.decode('ascii') + return text + except UnicodeDecodeError: + return email.header.Header(text, 'utf-8').encode() + + def attach(self, filename): + type, encoding = mimetypes.guess_type(filename) + if encoding or not type: + type = 'application/octet-stream' + vim.current.buffer.append('%s%s:%s'%(self._attachment_prefix, filename, type), 0) + + def send(self): + """ + Send the message in current buffer. + """ + b = vim.current.buffer + i = 0 + + # parse attachments + attachments = [] + while b[i].startswith(self._attachment_prefix): + filename, sep, type = b[i][len(self._attachment_prefix):].rpartition(':') + attachments.append((os.path.expanduser(filename), type)) + i += 1 + # skip the inline help + while b[i].startswith('Notmuch-Help:'): + i += 1 + + # add the headers + headers = {} + recipients = [] + from_addr = None + while i < len(b): + if not b[i]: + break + key, sep, val = b[i].partition(':') + i += 1 + + try: + key.decode('ascii') + except UnicodeDecodeError: + raise ValueError('Header name must be ASCII only.') + + if not val.strip(): + # skip empty headers + continue + + if key.lower() in ('to', 'cc', 'bcc'): + names, addrs = zip(*email.utils.getaddresses([val])) + names = map(self._encode_header, names) + + recipients += addrs + if key.lower() == 'bcc': + continue + val = ','.join(map(email.utils.formataddr, zip(names, addrs))) + else: + if key.lower() == 'from': + from_addr = email.utils.parseaddr(val)[1] + val = self._encode_header(val) + + headers[key] = val + + body = email.mime.text.MIMEText('\n'.join(b[i:]), 'plain', 'utf-8') + # add the body + if not attachments: + msg = body + else: + msg = email.mime.multipart.MIMEMultipart() + msg.attach(body) + for attachment in attachments: + maintype, subtype = attachment[1].split('/', 1) + + if maintype == 'text': + with open(attachment[0]) as f: + part = email.mime.text.MIMEText(f.read(), subtype, 'utf-8') + else: + if maintype == 'image': + obj = email.mime.image.MIMEImage + elif maintype == 'audio': + obj = email.mime.audio.MIMEAudio + else: + obj = email.mime.application.MIMEApplication + + with open(attachment[0]) as f: + part = obj(f.read(), subtype) + + part.add_header('Content-Disposition', 'attachment', + filename = os.path.basename(attachment[0])) + msg.attach(part) + + msg['User-Agent'] = vim.eval('g:nm_vimpy_user_agent') + msg['Message-ID'] = email.utils.make_msgid() + msg['Date'] = email.utils.formatdate(localtime = True) + for key in headers: + msg[key] = headers[key] + + # sanity checks + if not from_addr: + # XXX notmuch-python should export the user email address + raise ValueError('No sender address specified.') + if not recipients: + raise ValueError('No recipient specified.') + + # send + fcc = vim.eval('g:nm_vimpy_fcc_maildir') + if fcc: + dbroot = notmuch.Database().get_path() + mdir = mailbox.Maildir(os.path.join(dbroot, fcc)) + mdir.add(msg) + mdir.close() + # TODO configurable host + s = smtplib.SMTP('localhost') + ret = s.sendmail(from_addr, recipients, msg.as_string()) + for key in ret: + print 'Error sending mail to %s: %s'%(key, ret[key]) + s.quit() + +class RawMessage(NMBuffer): + + def __init__(self, msgid): + db = notmuch.Database() + msg = db.find_message(msgid) + + if msg: + with open(msg.get_filename()) as fp: + vim.current.buffer.append(fp.readlines()) + +class NMBufferElement(object): + """ + This object represents a structure (e.g. folder, thread or message) + corresponding to a range of lines in NMBuffer. + """ + # start and end lines, 0-based + start = None + end = None + # a string identifying the element + id = None + + def __init__(self, start, end, id): + self.start = start + self.end = end + self.id = id + +class Folder(NMBufferElement): + """ + A saved search / 'folder'. + """ + # folder name + name = None + + def __init__(self, start, end, id, name): + super(Folder, self).__init__(start, end, id) + self.name = name + +class Message(NMBufferElement): + + attachments = None + tags = None + + def __init__(self, start, end, id): + super(Message, self).__init__(start, end, id) + self.attachments = [] + +class Attachment(NMBufferElement): + data = None + + def __init__(self, start, end, data): + super(Attachment, self).__init__(start, end, '') + self. data = data + +#### global variables #### +# this dictionary stores the Python objects corresponding to notmuch-managed +# buffers, each indexed by the buffer number +nm_buffers = {} + +#### utility functions #### + +def get_current_buffer(): + """ + Get the NMBuffer object associated with current buffer or None. + """ + try: + return nm_buffers[vim.eval('bufnr("%")')] + except KeyError: + return None + +def delete_current_buffer(): + """ + Delete the NMBuffer associated with current buffer. + """ + del nm_buffers[vim.eval('bufnr("%")')] + +def get_relative_date(timestamp): + """ + Format a nice representation of 'time' relative to the current time. + + Examples include: + + 5 mins. ago (For times less than 60 minutes ago) + Today 12:30 (For times >60 minutes but still today) + Yest. 12:30 + Mon. 12:30 (Before yesterday but fewer than 7 days ago) + October 12 (Between 7 and 180 days ago (about 6 months)) + 2008-06-30 (More than 180 days ago) + + Shamelessly lifted from notmuch-time + TODO: this should probably be in python-notmuch + """ + try: + now = datetime.datetime.now() + then = datetime.datetime.fromtimestamp(timestamp) + except ValueError: + return 'when?' + + + if then > now: + return 'the future' + + delta = now - then + + if delta.days > 180: + return then.strftime("%F") # 2008-06-30 + + total_seconds = delta.seconds + delta.days * 24 * 3600 + if total_seconds < 3600: + return "%d min. ago"%(total_seconds / 60) + + if delta.days < 7: + if then.day == now.day: + return then.strftime("Today %R") # Today 12:30 + if then.day + 1 == now.day: + return then.strftime("Yest. %R") # Yest. 12:30 + return then.strftime("%a. %R") # Mon. 12:30 + + return then.strftime("%B %d") # October 12 + +def map_query(db, query, run): + """ + Execute runnable run on every message found for the specified query. + """ + def walk_query(message, run, level): + run(message, level) + level += 1 + replies = message.get_replies() + if replies is not None: + for reply in replies: + walk_query(reply, run, level) + + q = db.create_query(query) + for t in q.search_threads(): + for m in t.get_toplevel_messages(): + walk_query(m, run, 0) + +def decode_header(header): + """ + Decode a RFC 2822 header into a utf-8 string. + """ + ret = [] + for part in email.header.decode_header(header): + if part[1]: + try: + ret.append(part[0].decode(part[1]).encode('utf-8')) + continue + except LookupError: + print 'Unknown encoding: %s'%part[1] + ret.append(part[0]) + + return ' '.join(ret) + +#### functions for exporting stuff to viml #### + +def vim_get_tags(): + """ + Export a string listing all tags in the database, one per line into + a vim variable named 'taglist'. + """ + db = notmuch.Database() + tags = '\n'.join(db.get_all_tags()) + vim.command('let taglist = \'%s\''%tags) + +def vim_get_object(line, offset): + """ + Export start/end lines and id of the object that's offset objects + aways from given line into a dict named 'obj' in viml. + E.g. offset = 0 gets the object on the line, offset = 1 gets + next, offset = -1 previous. + This method is a noop if current line doesn't correspond to anything. + """ + # vim lines are 1-based + line -= 1 + assert line >= 0 + + obj = get_current_buffer().get_object(line, offset) + if obj: + vim.command('let obj = { "start" : %d, "end" : %d, "id" : "%s" }'%( + obj.start + 1, obj.end + 1, obj.id)) + +def vim_get_id(): + """ + Export the id string of current buffer into a vim string named 'buf_id'. + """ + id = get_current_buffer().id + vim.command('let buf_id = "%s"'%(id if id else '')) + +def vim_view_attachment(line): + """ + View an attachment corresponding to the given vim line + in an external viewer. + """ + line -= 1 # vim lines are 1-based + get_current_buffer().view_attachment(line) +def vim_save_attachment(line, filename): + """ + Save an attachment corresponding to the given vim line into given file. + """ + line -= 1 # vim lines are 1-based + get_current_buffer().save_attachment(line, filename) + +def vim_get_message_for_tag(tag): + """ + Export start line for the first message with the given tag into + a vim variable named 'start'. + """ + msg = get_current_buffer().get_message_for_tag(tag) + if msg: + start = msg.start + 1 + else: + start = 0 + + vim.command('let start = %d'%start) diff --git a/vim/plugin/notmuch-vimpy.vim b/vim/plugin/notmuch-vimpy.vim new file mode 100644 index 0000000..8d80a0e --- /dev/null +++ b/vim/plugin/notmuch-vimpy.vim @@ -0,0 +1,863 @@ +" nm-vimpy.vim plugin --- run notmuch within vim +" +" 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 . +" +" Based on the notmuch.vim plugin by: +" Authors: Bart Trojanowski +" Contributors: Felipe Contreras , +" Peter Hartman +" +" Mostly rewritten using python by Anton Khirnov +" + + +" --- initialization +if exists('s:nm_vimpy_loaded') || &cp + finish +endif + +" init the python layer +let s:python_path = expand(':p:h') +python import sys +exec "python sys.path += [r'" . s:python_path . "']" +python import vim, nm_vim + +command! NMVimpy call NMVimpy() + +" --- configuration defaults + +let s:nm_vimpy_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:nm_vimpy_fcc_maildir': 'sent' , + \ 'g:nm_vimpy_user_agent': 'notmuch-vimpy' , + \ } + +" defaults for g:nm_vimpy_folders +" override with: let g:nm_vimpy_folders = [ ... ] +let s:nm_vimpy_folders_defaults = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ ] + +let s:nm_vimpy_show_headers_defaults = [ + \ 'From', + \ 'To', + \ 'Cc', + \ 'Subject', + \ 'Date', + \ 'Reply-To', + \ 'Message-Id', + \] + +" defaults for g:nm_vimpy_compose_headers +" override with: let g:nm_vimpy_compose_headers = [ ... ] +let s:nm_vimpy_compose_headers_defaults = [ + \ 'From', + \ 'To', + \ 'Cc', + \ 'Bcc', + \ 'Subject' + \ ] + +" --- keyboard mapping definitions + +" --- --- bindings for folders mode {{{2 + +let g:nm_vimpy_folders_maps = { + \ 'm': ':call NM_new_mail()', + \ 's': ':call NM_search_prompt(0)', + \ 'q': ':call NM_kill_this_buffer()', + \ '=': ':call NM_folders_refresh_view()', + \ '': ':call NM_folders_show_search('''')', + \ '': ':call NM_folders_show_search(''tag:unread'')', + \ 'tt': ':call NM_folders_from_tags()', + \ } + +" --- --- bindings for search screen {{{2 +let g:nm_vimpy_search_maps = { + \ '': ':call NM_search_show_thread()', + \ '': ':call NM_search_show_thread_unread()', + \ '': ':call NM_search_expand('''')', + \ 'a': ':call NM_search_archive_thread()', + \ 'A': ':call NM_search_mark_read_then_archive_thread()', + \ 'D': ':call NM_search_delete_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(0)', + \ ',s': ':call NM_search_prompt(1)', + \ 'q': ':call NM_kill_this_buffer()', + \ '+': ':call NM_search_add_tags([])', + \ '-': ':call NM_search_remove_tags([])', + \ '=': ':call NM_search_refresh_view()', + \ } + +" --- --- bindings for show screen {{{2 +let g:nm_vimpy_show_maps = { + \ '': ':call NM_jump_message(-1)', + \ '': ':call NM_jump_message(+1)', + \ '': ':call NM_search_expand('''')', + \ 'q': ':call NM_kill_this_buffer()', + \ 's': ':call NM_search_prompt(0)', + \ + \ + \ 'a': ':call NM_show_archive_thread()', + \ 'A': ':call NM_show_mark_read_then_archive_thread()', + \ 'N': ':call NM_show_mark_read_then_next_open_message()', + \ 'v': ':call NM_show_view_all_mime_parts()', + \ '+': ':call NM_show_add_tag()', + \ '-': ':call NM_show_remove_tag()', + \ '': ':call NM_show_advance()', + \ '\|': ':call NM_show_pipe_message()', + \ + \ '': ':call NM_show_view_attachment()', + \ 'S': ':call NM_show_save_attachment()', + \ + \ 'r': ':call NM_show_reply()', + \ 'R': ':call NM_show_view_raw_message()', + \ 'm': ':call NM_new_mail()', + \ } + +" --- --- bindings for compose screen {{{2 +let g:nm_vimpy_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:nm_vimpy_raw_message_nmaps = { + \ 'q': ':call NM_kill_this_buffer()', + \ } + +" --- implement folders screen {{{1 + +" Create the folders buffer. +" Takes a list of [ folder name, query string] +function! s:NM_cmd_folders(folders) + call NM_create_buffer('folders') + silent 0put!=':view :view unread s:search =:refresh tt:all tags m:new mail' + python nm_vim.SavedSearches(vim.eval("a:folders")) + call NM_finalize_menu_buffer() + call NM_set_map('n', g:nm_vimpy_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 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 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 NM_create_buffer('search') + silent 0put!=printf(' Query results: %s', a:querystr) + if a:relative + python nm_vim.Search(querystr = vim.eval("a:querystr"), parent = nm_vim.nm_buffers[vim.eval('cur_buf')]) + else + python nm_vim.Search(querystr = vim.eval("a:querystr")) + endif + call NM_finalize_menu_buffer() + call NM_set_map('n', g:nm_vimpy_search_maps) +endfunction + +" --- --- search screen action functions {{{2 + +" Show the thread corresponding to current line +function! s:NM_search_show_thread() + let querystr = NM_search_thread_id() + if len(querystr) + call NM_cmd_show(querystr) + endif +endfunction + +" Same as NM_search_show_thread, except jump to first unread +function! s:NM_search_show_thread_unread() + call NM_search_show_thread() + python nm_vim.vim_get_message_for_tag('unread') + if start > 0 + exec printf('norm %dGzt', start) + silent! norm zo + 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,NM_search_type_completion') + if len(querystr) + call 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,NM_search_type_completion') + if len(querystr) + call NM_cmd_search(querystr, 1) + endif +endfunction + +""""""""""""""""""""""'' TODO +function! s:NM_search_archive_thread() + 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_delete_thread() + call 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 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, NM_search_thread_id()) + call add(cmd, 'AND') + call extend(cmd, [querystr]) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) +endfunction + +function! s:NM_search_add_tags(tags) + call NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags) +endfunction + +function! s:NM_search_remove_tags(tags) + call NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags) +endfunction + +function! s:NM_search_refresh_view() + let lno = line('.') + setlocal modifiable + norm 3ggdG + 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 + let text = input(a:prompt, '', 'custom,NM_tag_name_completion') + if !strlen(text) + return + endif + let tags = split(text, ' ') + else + let tags = a:intags + endif + call map(tags, 'a:prefix . v:val') + call NM_tag([], tags) +endfunction + +" --- implement show screen {{{1 + +function! s:NM_cmd_show(querystr) + "TODO: folding, syntax + call NM_create_buffer('show') + python nm_vim.ShowThread(vim.eval('a:querystr')) + + call NM_set_map('n', g:nm_vimpy_show_maps) + setlocal fillchars= + setlocal foldtext=NM_show_foldtext() + setlocal foldcolumn=6 + setlocal foldmethod=syntax + setlocal nomodifiable + setlocal nowrap + call NM_jump_message(1) +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 NM_kill_this_buffer() + if line('.') != line('$') + norm j + call NM_search_show_thread() + else + echo 'No more messages.' + endif +endfunction + +function! s:NM_show_archive_thread() + call NM_tag('', ['-inbox']) + call NM_show_next_thread() +endfunction + +function! s:NM_show_mark_read_then_archive_thread() + call NM_tag('', ['-unread', '-inbox']) + call 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:' . NM_show_message_id()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) +endfunction + +function! s:NM_show_view_all_mime_parts() + echo 'not implemented' +endfunction + +"Show the raw message for current line in a new buffer +function! s:NM_show_view_raw_message() + exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) + if !exists('obj') + return + endif + + call NM_create_buffer('rawmessage') + python nm_vim.RawMessage(vim.eval("obj['id']")) + call NM_set_map('n', g:nm_vimpy_raw_message_nmaps) +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 NM_tag(['id:' . obj['id']], advance_tags) + if obj['end'] == line('$') + call NM_kill_this_buffer() + else + call 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 + +function! s:NM_show_save_attachment() + let filename = input('Where to save the attachment: ', './', 'file') + if len(filename) + try + python nm_vim.vim_save_attachment(int(vim.eval('line(".")')), vim.eval('filename')) + echo 'Attachment saved successfully.' + endtry + endif +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 = NM_compose_get_user_email() + let hdrs['From'] = [ me ] + endif + + for key in g:nm_vimpy_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:nm_vimpy_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 NM_newComposeBuffer(lines, start_on_line) +endfunction + +function! s:NM_compose_send() + let fname = expand('%') + + try + python nm_vim.get_current_buffer().send() + call 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) + python nm_vim.get_current_buffer().attach(vim.eval('attachment')) + endif +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_create_buffer(type) + let prev_bufnr = bufnr('%') + + enew + setlocal buftype=nofile + setlocal bufhidden=hide + execute printf('set filetype=nm_vimpy-%s', a:type) + execute printf('set syntax=nm_vimpy-%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 = 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: - 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:nm_vimpy_signature') + call extend(lines, ['', '-- ']) + call extend(lines, g:nm_vimpy_signature) + endif + + + let prev_bufnr = bufnr('%') + call NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail', + \ 'compose', lines) + let b:nm_prev_bufnr = prev_bufnr + + call NM_set_map('n', g:nm_vimpy_compose_nmaps) + + 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 + + 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 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 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 : [NM_search_thread_id()] + if !len(filter) + throw 'Eeek! I couldn''t find the thead id!' + endif + python nm_vim.get_current_buffer().tag(tags = vim.eval("a:tags"), querystr = vim.eval('join(filter)')) +endfunction + +" --- process and set the defaults {{{1 + +function! s:NM_set_defaults(force) + setlocal bufhidden=hide + for [key, dflt] in items(s:nm_vimpy_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:nm_vimpy_folders') + let g:nm_vimpy_folders = s:nm_vimpy_folders_defaults +endif + +if !exists('g:nm_vimpy_show_headers') + let g:nm_vimpy_show_headers = s:nm_vimpy_show_headers_defaults +endif + +if !exists('g:nm_vimpy_signature') + if filereadable(glob('~/.signature')) + let g:nm_vimpy_signature = readfile(glob('~/.signature')) + endif +endif +if !exists('g:nm_vimpy_compose_headers') + let g:nm_vimpy_compose_headers = s:nm_vimpy_compose_headers_defaults +endif + +" --- assign keymaps {{{1 + +function! s:NM_set_map(type, maps) + for [key, code] in items(a:maps) + exec printf('%snoremap %s %s', a:type, key, code) + endfor +endfunction + +" --- command handler {{{1 + +function! NMVimpy() + call NM_cmd_folders(g:nm_vimpy_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 indentlevel = matchstr(getline(v:foldstart), '^[0-9]\+') + return repeat(' ', indentlevel) . getline(v:foldstart + 1) +endfunction + +"Completion of search prompt +function! NM_search_type_completion(arg_lead, cmd_line, cursor_pos) + let keywords = 'from:' . "\n" . + \ 'to:' . "\n" . + \ 'subject:' . "\n" . + \ 'attachment:' . "\n" . + \ 'tag:' . "\n" . + \ 'id:' . "\n" . + \ 'thread:' . "\n" . + \ 'folder:' . "\n" . + \ 'and' . "\n" . + \ 'or' + let s_idx = strridx(a:arg_lead, " ") + let col_idx = stridx(a:arg_lead, ":", s_idx) + let prefix = strpart(a:arg_lead, 0, s_idx + 1) + if col_idx < 0 + return prefix . substitute(keywords, "\n", "\n" . prefix, "g") + endif + if stridx(a:arg_lead, 'tag:', s_idx) >= 0 + python nm_vim.vim_get_tags() + return prefix . 'tag:' . substitute(taglist, "\n", "\n" . prefix . 'tag:', "g") + endif + return '' +endfunction + +function! NM_tag_name_completion(arg_lead, cmd_line, cursor_pos) + let s_idx = strridx(a:arg_lead, " ") + let prefix = strpart(a:arg_lead, 0, s_idx + 1) + python nm_vim.vim_get_tags() + return prefix . substitute(taglist, "\n", "\n" . prefix, "g") +endfunction + +let s:notmuch_loaded = 1 diff --git a/vim/syntax/nm_vimpy-compose.vim b/vim/syntax/nm_vimpy-compose.vim new file mode 100644 index 0000000..19adb75 --- /dev/null +++ b/vim/syntax/nm_vimpy-compose.vim @@ -0,0 +1,7 @@ +runtime! syntax/mail.vim + +syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!' +syntax match nmComposeHelpLine /Notmuch-Help:/ contained + +highlight link nmComposeHelp Include +highlight link nmComposeHelpLine Error diff --git a/vim/syntax/nm_vimpy-folders.vim b/vim/syntax/nm_vimpy-folders.vim new file mode 100644 index 0000000..fab6956 --- /dev/null +++ b/vim/syntax/nm_vimpy-folders.vim @@ -0,0 +1,15 @@ +" 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 + diff --git a/vim/syntax/nm_vimpy-search.vim b/vim/syntax/nm_vimpy-search.vim new file mode 100644 index 0000000..4a19329 --- /dev/null +++ b/vim/syntax/nm_vimpy-search.vim @@ -0,0 +1,18 @@ +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,nmUnread +syntax match nmSearchTags /.\+$/ contained +syntax match nmUnread /.*\.*)$/ contained + +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 diff --git a/vim/syntax/nm_vimpy-show.vim b/vim/syntax/nm_vimpy-show.vim new file mode 100644 index 0000000..d21eb45 --- /dev/null +++ b/vim/syntax/nm_vimpy-show.vim @@ -0,0 +1,23 @@ +" 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 -- cgit v1.2.3