From 590c0763363389a976c74852919554a994c73752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=A4min=2Eid?= <8972331+baumea@users.noreply.github.com> Date: Mon, 19 May 2025 21:24:21 +0200 Subject: [PATCH 1/2] Improved user experience Extended autocompletion: - `search_terms_list` now also contains `is:`, `to:`, `from`, `body:` and the logical expressions `and`, `or`, `not`. - Added autocomplete functionality for `mimetype:`, `to:`, `from:`, `path:`, and `folder:` search. - Extended `:ComposeMail` to optionally accept recipient, incl. autocomplete Key binding and visual control - Extended `:TagAdd`, `:TagRm`, and `:TagToggle` to accept line ranges - Extended key bindings `+`, `-`, `=`, `a`, `A`, and `x` in the threads view to the visual mode - Added `f` key binding (visual mode and normal mode) in the threads view to toggle the `flagged` tag --- autoload/notmuch.vim | 31 +++++++++++++- ftplugin/notmuch-threads.vim | 16 ++++++-- lua/notmuch/init.lua | 4 +- lua/notmuch/refresh.lua | 5 ++- lua/notmuch/send.lua | 7 +++- lua/notmuch/tag.lua | 78 +++++++++++++++++++++--------------- plugin/notmuch.vim | 5 ++- 7 files changed, 102 insertions(+), 44 deletions(-) diff --git a/autoload/notmuch.vim b/autoload/notmuch.vim index 67522e9..dc5ad4b 100644 --- a/autoload/notmuch.vim +++ b/autoload/notmuch.vim @@ -1,12 +1,38 @@ let s:search_terms_list = [ "attachment:", "folder:", "id:", "mimetype:", \ "property:", "subject:", "thread:", "date:", "from:", "lastmod:", - \ "path:", "query:", "tag:", "to:" ] + \ "path:", "query:", "tag:", "is:", "to:", "body:", "and ", "or ", "not " ] function! notmuch#CompSearchTerms(ArgLead, CmdLine, CursorPos) abort if match(a:ArgLead, "tag:") != -1 let l:tag_list = split(system('notmuch search --output=tags "*"'), '\n') return "tag:" .. join(l:tag_list, "\ntag:") endif + if match(a:ArgLead, "is:") != -1 + let l:is_list = split(system('notmuch search --output=tags "*"'), '\n') + return "is:" .. join(l:is_list, "\nis:") + endif + if match(a:ArgLead, "mimetype:") != -1 + let l:mimetype_list = ["application/", "audio/", "chemical/", + \ "font/", "image/", "inode/", "message/", "model/", + \ "multipart/", "text/", "video/"] + return "mimetype:" .. join(l:mimetype_list, "\nmimetype:") + endif + if match(a:ArgLead, "from:") != -1 + let l:from_list = split(system('notmuch address "*"'), '\n') + return "from:" .. join(l:from_list, "\nfrom:") + endif + if match(a:ArgLead, "to:") != -1 + let l:to_list = split(system('notmuch address "*"'), '\n') + return "to:" .. join(l:to_list, "\nto:") + endif + if match(a:ArgLead, "folder:") != -1 + let l:folder_list = split(system('find ' .. g:notmuch_mailroot .. ' -type d -name cur -print0| sed -n -z "s|^' .. g:notmuch_mailroot .. '/*||p" | xargs -0 dirname | sort | uniq'), '\n') + return "folder:" .. join(l:folder_list, "\nfolder:") + endif + if match(a:ArgLead, "path:") != -1 + let l:path_list = split(system('find ' .. g:notmuch_mailroot .. ' -type d -print0| sed -n -z "s|^' .. g:notmuch_mailroot .. '/*||p" | sort -z | uniq -z | tr "\0" "\n"'), '\n') + return "path:" .. join(l:path_list, "\npath:") + endif return join(s:search_terms_list, "\n") endfunction @@ -14,4 +40,7 @@ function! notmuch#CompTags(ArgLead, CmdLine, CursorPos) abort return system('notmuch search --output=tags "*"') endfunction +function! notmuch#CompAddress(ArgLead, CmdLine, CursorPos) abort + return system('notmuch address "*"') +endfunction " vim: tabstop=2:shiftwidth=2:expandtab diff --git a/ftplugin/notmuch-threads.vim b/ftplugin/notmuch-threads.vim index ffcab7e..b39853b 100644 --- a/ftplugin/notmuch-threads.vim +++ b/ftplugin/notmuch-threads.vim @@ -5,18 +5,26 @@ let r = v:lua.require('notmuch.refresh') let s = v:lua.require('notmuch.sync') let tag = v:lua.require('notmuch.tag') -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagAdd :call tag.thread_add_tag("") -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagRm :call tag.thread_rm_tag("") -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagToggle :call tag.thread_toggle_tag("") +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagAdd :call tag.thread_add_tag(, , ) +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagRm :call tag.thread_rm_tag(, , ) +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagToggle :call tag.thread_toggle_tag(, , ) nmap :call nm.show_thread() nmap r :call r.refresh_search_buffer() nmap q :bwipeout nmap % :call s.sync_maildir() nmap + :TagAdd +xmap + :TagAdd nmap - :TagRm +xmap - :TagRm nmap = :TagToggle +xmap = :TagToggle nmap a :TagToggle inboxj +xmap a :TagToggle inbox nmap A :TagRm inbox unreadj -nmap x :TagToggle unreadj +xmap A :TagRm inbox unread +nmap x :TagToggle unread +xmap x :TagToggle unread +nmap f :TagToggle flaggedj +xmap f :TagToggle flagged nmap C :call v:lua.require('notmuch.send').compose() diff --git a/lua/notmuch/init.lua b/lua/notmuch/init.lua index 2e8697d..167e305 100644 --- a/lua/notmuch/init.lua +++ b/lua/notmuch/init.lua @@ -57,10 +57,11 @@ end -- -- @param search string: search terms matching format from -- `notmuch-search-terms(7)` +-- @param jumptothreadid string: jump to thread id after search -- -- @usage -- lua require('notmuch').search_terms('tag:inbox') -nm.search_terms = function(search) +nm.search_terms = function(search, jumptothreadid) local num_threads_found = 0 if search == '' then return nil @@ -85,6 +86,7 @@ nm.search_terms = function(search) -- Completion logic if vim.fn.getline(2) ~= '' then num_threads_found = vim.fn.line('$') - 1 end print('Found ' .. num_threads_found .. ' threads') + vim.fn.search(jumptothreadid) end) -- Set cursor at head of buffer, declare filetype, and disable modifying diff --git a/lua/notmuch/refresh.lua b/lua/notmuch/refresh.lua index 8980a23..8a2d353 100644 --- a/lua/notmuch/refresh.lua +++ b/lua/notmuch/refresh.lua @@ -12,9 +12,12 @@ local nm = require('notmuch') -- -- Normally invoked by pressing `r` in the search results buffer -- lua require('notmuch.refresh').refresh_search_buffer() r.refresh_search_buffer = function() + local line = v.nvim_get_current_line() + local threadid = string.match(line, "%S+", 8) local search = string.match(v.nvim_buf_get_name(0), '%a+:%C+') v.nvim_command('bwipeout') - nm.search_terms(search) + nm.search_terms(search, threadid) + vim.fn.search(threadid) end -- Refreshes the thread view buffer diff --git a/lua/notmuch/send.lua b/lua/notmuch/send.lua index 42703de..c805522 100644 --- a/lua/notmuch/send.lua +++ b/lua/notmuch/send.lua @@ -88,15 +88,18 @@ end -- message headers and body. The mail content is stored in `/tmp/` so the user -- can come back to it later if needed. -- +-- @param to string: recipient address (optionaal argument) +-- -- @usage -- -- Typically you can run this with `:ComposeMail` or pressing `C` -- require('notmuch.send').compose() -s.compose = function() +s.compose = function(to) + to = to or '' local compose_filename = '/tmp/compose.eml' -- TODO: Add ability to modify default body message and signature local headers = { - 'To: ', + 'To: ' .. to, 'Cc: ', 'Subject: ', '', diff --git a/lua/notmuch/tag.lua b/lua/notmuch/tag.lua index 7dc0c08..f0d2613 100644 --- a/lua/notmuch/tag.lua +++ b/lua/notmuch/tag.lua @@ -49,52 +49,64 @@ t.msg_toggle_tag = function(tags) db.close() end -t.thread_add_tag = function(tags) +t.thread_add_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - for i,tag in pairs(t) do - thread:add_tag(tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + for i,tag in pairs(t) do + thread:add_tag(tag) + end + db.close() end - db.close() print('+(' .. tags .. ')') end -t.thread_rm_tag = function(tags) +t.thread_rm_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - for i,tag in pairs(t) do - thread:rm_tag(tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + for i,tag in pairs(t) do + thread:rm_tag(tag) + end + db.close() end - db.close() print('-(' .. tags .. ')') end -t.thread_toggle_tag = function(tags) +t.thread_toggle_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - local curr_tags = thread:get_tags() - for i,tag in pairs(t) do - if curr_tags[tag] == true then - thread:rm_tag(tag) - print('-' .. tag) - else - thread:add_tag(tag) - print('+' .. tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + local curr_tags = thread:get_tags() + for i,tag in pairs(t) do + if curr_tags[tag] == true then + thread:rm_tag(tag) + print("-" .. tag) + else + thread:add_tag(tag) + print("+" .. tag) + end end + db.close() end - db.close() end return t diff --git a/plugin/notmuch.vim b/plugin/notmuch.vim index aeaf817..b64ae2f 100644 --- a/plugin/notmuch.vim +++ b/plugin/notmuch.vim @@ -1,4 +1,5 @@ -command -complete=custom,notmuch#CompSearchTerms -nargs=* NmSearch :call v:lua.require('notmuch').search_terms("") -command ComposeMail :call v:lua.require('notmuch.send').compose() +let g:notmuch_mailroot = trim(system('notmuch config get database.mail_root')) +command -complete=custom,notmuch#CompSearchTerms -nargs=* NmSearch :call v:lua.require('notmuch').search_terms() +command -complete=custom,notmuch#CompAddress -nargs=* ComposeMail :call v:lua.require('notmuch.send').compose() " vim: tabstop=2:shiftwidth=2:expandtab From a9ee5964fd970835e3ab95952df8b33bb9f57c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=A4min=2Eid?= <8972331+baumea@users.noreply.github.com> Date: Mon, 19 May 2025 21:24:36 +0200 Subject: [PATCH 2/2] Add functionality to delete emails - Adds the command `:DelThread` in the thread view. This command adds the tag "del" to the emails. This command also works in visual mode, so multiple threads can be deleted simultaneously. - Binds `:DelThread` to `dd` in normal mode and `d` in visual mode. - Update the hint text to display `dd` key. - Binds `D` to the function call `purge_del()`. With this, emails marked to be deleted are removed from the drive. After hitting `D`, the user sees all emails marked to be delete. Puring is then initiated by hitting `DD` (see line 8 in `lua/notmuch/delete.lua`) --- ftplugin/notmuch-threads.vim | 4 ++++ lua/notmuch/delete.lua | 31 +++++++++++++++++++++++++++++++ lua/notmuch/init.lua | 2 +- syntax/notmuch-threads.vim | 2 +- 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 lua/notmuch/delete.lua diff --git a/ftplugin/notmuch-threads.vim b/ftplugin/notmuch-threads.vim index b39853b..2208ca6 100644 --- a/ftplugin/notmuch-threads.vim +++ b/ftplugin/notmuch-threads.vim @@ -8,6 +8,7 @@ let tag = v:lua.require('notmuch.tag') command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagAdd :call tag.thread_add_tag(, , ) command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagRm :call tag.thread_rm_tag(, , ) command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagToggle :call tag.thread_toggle_tag(, , ) +command -buffer -range DelThread :call tag.thread_add_tag("del", , ) | :call tag.thread_rm_tag("inbox", , ) nmap :call nm.show_thread() nmap r :call r.refresh_search_buffer() @@ -28,3 +29,6 @@ xmap x :TagToggle unread nmap f :TagToggle flaggedj xmap f :TagToggle flagged nmap C :call v:lua.require('notmuch.send').compose() +nmap dd :DelThreadj +xmap d :DelThread +nmap D :lua require('notmuch.delete').purge_del() diff --git a/lua/notmuch/delete.lua b/lua/notmuch/delete.lua new file mode 100644 index 0000000..c7a6346 --- /dev/null +++ b/lua/notmuch/delete.lua @@ -0,0 +1,31 @@ +local d = {} +local v = vim.api +local nm = require("notmuch") +local r = require("notmuch.refresh") + +local confirm_purge = function() + -- remove keymap + vim.keymap.del("n", "DD", { buffer = true }) + -- Confirm + local choice = v.nvim_call_function("confirm", { + "Purge deleted emails?", + "&Yes\n&No", + 2, -- Default to no + }) + + if choice == 1 then + v.nvim_command("silent ! notmuch search --output=files --format=text0 tag:del and tag:/./ | xargs -0 rm") + v.nvim_command("silent ! notmuch new") + r.refresh_search_buffer() + end +end + +d.purge_del = function() + nm.search_terms("tag:del and tag:/./") + -- Set keymap for purgin + vim.keymap.set("n", "DD", function() + confirm_purge() + end, { buffer = true }) +end + +return d diff --git a/lua/notmuch/init.lua b/lua/notmuch/init.lua index 167e305..79fa4d6 100644 --- a/lua/notmuch/init.lua +++ b/lua/notmuch/init.lua @@ -78,7 +78,7 @@ nm.search_terms = function(search, jumptothreadid) v.nvim_buf_set_name(buf, search) v.nvim_win_set_buf(0, buf) - local hint_text = "Hints: : Open thread | q: Close | r: Refresh | %: Sync maildir | a: Archive | A: Archive and Read | +: Add tag | -: Remove tag | =: Toggle tag" + local hint_text = "Hints: : Open thread | q: Close | r: Refresh | %: Sync maildir | a: Archive | A: Archive and Read | +/-/=: Add, remove, toggle tag | dd: Delete" v.nvim_buf_set_lines(buf, 0, 2, false, { hint_text , "" }) -- Async notmuch search to make the UX non blocking diff --git a/syntax/notmuch-threads.vim b/syntax/notmuch-threads.vim index 1b40ffd..9c582a4 100644 --- a/syntax/notmuch-threads.vim +++ b/syntax/notmuch-threads.vim @@ -10,7 +10,7 @@ syntax region nmHints start=/^Hints:/ end=/$/ oneline contains=nmHintsIdentifi syntax match nmHintsIdentifier "^Hints:" contained nextgroup=nmHintsKey syntax match nmHintsKey "\s\+[^:\s]\+" contained nextgroup=nmHintsKVDelimiter syntax match nmHintsKVDelimiter ":" contained nextgroup=nmHintsValue -syntax match nmHintsValue "\s\+[A-Za-z0-9\ ]\+" contained nextgroup=nmHintsDelimiter +syntax match nmHintsValue "\s\+[A-Za-z0-9\ ,.]\+" contained nextgroup=nmHintsDelimiter syntax match nmHintsDelimiter "|" contained nextgroup=nmHintsKey highlight link nmHintsIdentifier Comment