diff --git a/mu4e/mu4e-headers.el b/mu4e/mu4e-headers.el index fb5930d0..503c6133 100644 --- a/mu4e/mu4e-headers.el +++ b/mu4e/mu4e-headers.el @@ -39,6 +39,7 @@ (require 'mu4e-vars) (require 'mu4e-mark) (require 'mu4e-context) +(require 'mu4e-search) (require 'mu4e-compose) (require 'mu4e-actions) (require 'mu4e-message) @@ -121,27 +122,6 @@ indexing operation showed changes." :type 'boolean :group 'mu4e-headers) -(defcustom mu4e-headers-results-limit 500 - "Maximum number of results to show; this affects performance -quite a bit, especially when `mu4e-headers-include-related' is -non-nil. Set to -1 for no limits, and you temporarily (for one -query) ignore the limit by pressing a C-u before invoking the -search. - -Note that there are a few complications when -`mu4e-headers-include-related' is enabled: mu performs *two* -queries; the first one with this limit set, and then a second -(unlimited) query for all messages that are related to the first -matches. We then limit this second result as well, favoring the -messages that were found in the first set (the \"leaders\"). -" - :type '(choice (const :tag "Unlimited" -1) - (integer :tag "Limit")) - :group 'mu4e-headers) - -(make-obsolete-variable 'mu4e-search-results-limit - 'mu4e-headers-results-limit "0.9.9.5-dev6") - (defcustom mu4e-headers-advance-after-mark t "With this option set to non-nil, automatically advance to the next mail after marking a message in header view." @@ -187,33 +167,6 @@ query have been received and are displayed." :type 'hook :group 'mu4e-headers) -(defcustom mu4e-headers-search-bookmark-hook nil - "Hook run just after we invoke a bookmarked search. This -function receives the query as its parameter, before any -rewriting as per `mu4e-query-rewrite-function' has taken place. - -The reason to use this instead of `mu4e-headers-search-hook' is -if you only want to execute a hook when a search is entered via a -bookmark, e.g. if you'd like to treat the bookmarks as a custom -folder and change the options for the search, e.g. -`mu4e-headers-show-threads', `mu4e-headers-include-related', -`mu4e-headers-skip-duplicates` or `mu4e-headers-results-limit'. -" - :type 'hook - :group 'mu4e-headers) - -(defcustom mu4e-headers-search-hook nil - "Hook run just before executing a new search operation. This -function receives the query as its parameter, before any -rewriting as per `mu4e-query-rewrite-function' has taken place - -This is a more general hook facility than the -`mu4e-headers-search-bookmark-hook'. It gets called on every -executed search, not just those that are invoked via bookmarks, -but also manually invoked searches." - :type 'hook - :group 'mu4e-headers) - ;;; Public variables (defvar mu4e-headers-sort-field :date @@ -309,14 +262,6 @@ and (optionally) PARAM, and should return non-nil when there's a match. * PARAM-FUNC is function that is evaluated once, and its value is then passed to PREDICATE-FUNC as PARAM. This is useful for getting user-input.") - -(defvar mu4e-headers-show-threads t - "Whether to show threads in the headers list.") - -(defvar mu4e-headers-full-search nil - "Whether to show all results. -If this is nil show results up to `mu4e-headers-results-limit')") - ;;; Internal variables/constants ;; docid cookies @@ -795,6 +740,43 @@ if provided, or at the end of the buffer otherwise." (mu4e~headers-add-header line (mu4e-message-field msg :docid) point msg)))))) + + + +;;; Performing queries (internal) +(defun mu4e--search-execute (expr ignore-history) + "Search for query EXPR. + +Switch to the output buffer for the results. If IGNORE-HISTORY is +true, do *not* update the query history stack." + (let* ((buf (get-buffer-create mu4e~headers-buffer-name)) + (inhibit-read-only t) + (rewritten-expr (funcall mu4e-query-rewrite-function expr)) + (maxnum (unless mu4e-search-full mu4e-search-results-limit))) + (with-current-buffer buf + (mu4e-headers-mode) + (unless ignore-history + ;; save the old present query to the history list + (when mu4e--search-last-query + (mu4e--search-push-query mu4e--search-last-query 'past))) + (setq mu4e--search-last-query rewritten-expr) + (mu4e~headers-update-mode-line)) + + ;; when the buffer is already visible, select it; otherwise, + ;; switch to it. + (unless (get-buffer-window buf 0) + (switch-to-buffer buf)) + (run-hook-with-args 'mu4e-search-hook expr) + (mu4e~headers-clear mu4e~search-message) + (mu4e~proc-find + rewritten-expr + mu4e-headers-show-threads + mu4e-headers-sort-field + mu4e-headers-sort-direction + maxnum + mu4e-headers-skip-duplicates + mu4e-headers-include-related))) + (defconst mu4e~search-message "Searching...") (defconst mu4e~no-matches "No matching messages found") (defconst mu4e~end-of-results "End of search results") @@ -888,20 +870,8 @@ after the end of the search results." ;; for terminal users (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) - (define-key map "s" 'mu4e-headers-search) - (define-key map "S" 'mu4e-headers-search-edit) - - (define-key map "/" 'mu4e-headers-search-narrow) - (define-key map "j" 'mu4e~headers-jump-to-maildir) - (define-key map (kbd "") 'mu4e-headers-query-prev) - (define-key map (kbd "\\") 'mu4e-headers-query-prev) - (define-key map (kbd "") 'mu4e-headers-query-next) - - (define-key map "b" 'mu4e-headers-search-bookmark) - (define-key map "B" 'mu4e-headers-search-bookmark-edit) - (define-key map "O" 'mu4e-headers-change-sorting) (define-key map "P" 'mu4e-headers-toggle-threading) (define-key map "Q" 'mu4e-headers-toggle-full-search) @@ -1121,7 +1091,7 @@ no user-interaction ongoing." ;; otherwise we'd trigger a headers view from out of nowhere. (when (and (buffer-live-p (mu4e-get-headers-buffer)) (window-live-p (get-buffer-window (mu4e-get-headers-buffer) t))) - (mu4e-headers-rerun-search)))) + (mu4e-search-rerun)))) (define-derived-mode mu4e-headers-mode special-mode "mu4e:headers" @@ -1145,6 +1115,7 @@ no user-interaction ongoing." (mu4e~mark-initialize) ;; initialize the marking subsystem (mu4e-context-minor-mode) + (mu4e-search-minor-mode) (hl-line-mode 1)) (defun mu4e~headers-index-updated-hook-fn () @@ -1256,7 +1227,7 @@ docid is not found." (name "mu4e-headers")) (setq mode-name name) - (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e~headers-last-query)) + (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e--search-last-query)) (make-local-variable 'global-mode-string) @@ -1275,41 +1246,6 @@ docid is not found." "")))))) -(defun mu4e~headers-search-execute (expr ignore-history) - "Search in the mu database for EXPR, and switch to the output -buffer for the results. If IGNORE-HISTORY is true, do *not* update -the query history stack." - ;; note: we don't want to update the history if this query comes from - ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'. - ;;(mu4e-hide-other-mu4e-buffers) - (let* ((buf (get-buffer-create mu4e~headers-buffer-name)) - (inhibit-read-only t) - (rewritten-expr (funcall mu4e-query-rewrite-function expr)) - (maxnum (unless mu4e-headers-full-search mu4e-headers-results-limit))) - (with-current-buffer buf - (mu4e-headers-mode) - (unless ignore-history - ;; save the old present query to the history list - (when mu4e~headers-last-query - (mu4e~headers-push-query mu4e~headers-last-query 'past))) - (setq mu4e~headers-last-query rewritten-expr) - (mu4e~headers-update-mode-line)) - - ;; when the buffer is already visible, select it; otherwise, - ;; switch to it. - (unless (get-buffer-window buf 0) - (switch-to-buffer buf)) - (run-hook-with-args 'mu4e-headers-search-hook expr) - (mu4e~headers-clear mu4e~search-message) - (mu4e~proc-find - rewritten-expr - mu4e-headers-show-threads - mu4e-headers-sort-field - mu4e-headers-sort-direction - maxnum - mu4e-headers-skip-duplicates - mu4e-headers-include-related))) - (defun mu4e~headers-redraw-get-view-window () "Close all windows, redraw the headers buffer based on the value of `mu4e-split-view', and return a window for the message view." @@ -1503,166 +1439,8 @@ descendants." (let ((current-prefix-arg t)) (call-interactively 'mu4e-headers-mark-thread)))) - -;;; The query past / present / future - -(defvar mu4e~headers-query-past nil - "Stack of queries before the present one.") -(defvar mu4e~headers-query-future nil - "Stack of queries after the present one.") -(defvar mu4e~headers-query-stack-size 20 - "Maximum size for the query stacks.") - -(defun mu4e~headers-push-query (query where) - "Push QUERY to one of the query stacks. -WHERE is a symbol telling us where to push; it's a symbol, either -'future or 'past. Functional also removes duplicates, limits the -stack size." - (let ((stack - (cl-case where - (past mu4e~headers-query-past) - (future mu4e~headers-query-future)))) - ;; only add if not the same item - (unless (and stack (string= (car stack) query)) - (push query stack) - ;; limit the stack to `mu4e~headers-query-stack-size' elements - (when (> (length stack) mu4e~headers-query-stack-size) - (setq stack (cl-subseq stack 0 mu4e~headers-query-stack-size))) - ;; remove all duplicates of the new element - (cl-remove-if (lambda (elm) (string= elm (car stack))) (cdr stack)) - ;; update the stacks - (cl-case where - (past (setq mu4e~headers-query-past stack)) - (future (setq mu4e~headers-query-future stack)))))) - -(defun mu4e~headers-pop-query (whence) - "Pop a query from the stack. -WHENCE is a symbol telling us where to get it from, either `future' -or `past'." - (cl-case whence - (past - (unless mu4e~headers-query-past - (mu4e-warn "No more previous queries")) - (pop mu4e~headers-query-past)) - (future - (unless mu4e~headers-query-future - (mu4e-warn "No more next queries")) - (pop mu4e~headers-query-future)))) - - -;;; Reading queries with completion - -(defvar mu4e-minibuffer-search-query-map - (let ((map (copy-keymap minibuffer-local-map))) - (define-key map (kbd "TAB") #'completion-at-point) - map) - - "The keymap when reading a search query.") -(defun mu4e-read-query (prompt &optional initial-input) - "Read a search query with completion using PROMPT and INITIAL-INPUT." - (minibuffer-with-setup-hook - (lambda () - (setq-local completion-at-point-functions - #'mu4e~search-query-competion-at-point) - (use-local-map mu4e-minibuffer-search-query-map)) - (read-string prompt initial-input 'mu4e~headers-search-hist))) - -(defvar mu4e~headers-search-hist nil - "History list of searches.") - -(defconst mu4e~search-query-keywords - '("and" "or" "not" - "from:" "to:" "cc:" "bcc:" "contact:" "date:" "subject:" "body:" - "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:" - "size:" "embed:")) - -(defun mu4e~search-query-competion-at-point () - (cond - ((not (looking-back "[:\"][^ \t]*" nil)) - (let ((bounds (bounds-of-thing-at-point 'word))) - (list (or (car bounds) (point)) - (or (cdr bounds) (point)) - mu4e~search-query-keywords))) - ((looking-back "flag:\\(\\w*\\)" nil) - (list (match-beginning 1) - (match-end 1) - '("attach" "draft" "flagged" "list" "new" "passed" "replied" - "seen" "trashed" "unread" "encrypted" "signed"))) - ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil) - (list (match-beginning 1) - (match-end 1) - (mu4e-get-maildirs))) - ((looking-back "prio:\\(\\w*\\)" nil) - (list (match-beginning 1) - (match-end 1) - (list "high" "normal" "low"))) - ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil) - (list (match-beginning 1) - (match-end 1) - (mailcap-mime-types))))) - ;;; Interactive functions - -(defun mu4e-headers-search (&optional expr prompt edit - ignore-history msgid show) - "Search in the mu database for EXPR, and switch to the output -buffer for the results. This is an interactive function which ask -user for EXPR. PROMPT, if non-nil, is the prompt used by this -function (default is \"Search for:\"). If EDIT is non-nil, -instead of executing the query for EXPR, let the user edit the -query before executing it. If IGNORE-HISTORY is true, do *not* -update the query history stack. If MSGID is non-nil, attempt to -move point to the first message with that message-id after -searching. If SHOW is non-nil, show the message with MSGID." - ;; note: we don't want to update the history if this query comes from - ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'." - (interactive) - (let* ((prompt (mu4e-format (or prompt "Search for: "))) - (expr - (if (or (null expr) edit) - (mu4e-read-query prompt expr) - expr))) - (mu4e-mark-handle-when-leaving) - (mu4e~headers-search-execute expr ignore-history) - (setq mu4e~headers-msgid-target msgid - mu4e~headers-view-target show))) - -(defun mu4e-headers-search-edit () - "Edit the last search expression." - (interactive) - (mu4e-headers-search mu4e~headers-last-query nil t)) - -(defun mu4e-headers-search-bookmark (&optional expr edit) - "Search using some bookmarked query EXPR. -If EDIT is non-nil, let the user edit the bookmark before starting -the search." - (interactive) - (let ((expr - (or expr - (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: "))))) - (run-hook-with-args 'mu4e-headers-search-bookmark-hook expr) - (mu4e-headers-search expr (when edit "Edit bookmark: ") edit))) - -(defun mu4e-headers-search-bookmark-edit () - "Edit an existing bookmark before executing it." - (interactive) - (mu4e-headers-search-bookmark nil t)) - -(defun mu4e-headers-search-narrow (filter ) - "Narrow the last search by appending search expression FILTER to -the last search expression. Note that you can go back to previous -query (effectively, 'widen' it), with `mu4e-headers-query-prev'." - (interactive - (let ((filter - (read-string (mu4e-format "Narrow down to: ") - nil 'mu4e~headers-search-hist nil t))) - (list filter))) - (unless mu4e~headers-last-query - (mu4e-warn "There's nothing to filter")) - (mu4e-headers-search - (format "(%s) AND (%s)" mu4e~headers-last-query filter))) - (defun mu4e-headers-change-sorting (&optional field dir) "Change the sorting/threading parameters. FIELD is the field to sort by; DIR is a symbol: either 'ascending, @@ -1698,7 +1476,7 @@ sortfield, change the sort-order) or nil (ask the user)." (mu4e-message "Sorting by %s (%s)" (symbol-name sortfield) (symbol-name mu4e-headers-sort-direction)) - (mu4e-headers-rerun-search))) + (mu4e-search-rerun))) (defun mu4e~headers-toggle (name togglevar dont-refresh) "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil, @@ -1710,7 +1488,7 @@ re-run the last search." (if dont-refresh " (press 'g' to refresh)" "")) (unless dont-refresh - (mu4e-headers-rerun-search))) + (mu4e-search-rerun))) (defun mu4e-headers-toggle-threading (&optional dont-refresh) "Toggle `mu4e-headers-show-threads'. With prefix-argument, do @@ -1792,42 +1570,6 @@ window . " ;; (mu4e~proc-view dowcid decrypt)) (mu4e~proc-view docid mark-as-read decrypt verify))) -(defun mu4e-headers-rerun-search () - "Rerun the search for the last search expression." - (interactive) - ;; if possible, try to return to the same message - (let* ((msg (mu4e-message-at-point t)) - (msgid (and msg (mu4e-message-field msg :message-id)))) - (mu4e-headers-search mu4e~headers-last-query nil nil t msgid))) - -(defun mu4e~headers-query-navigate (whence) - "Execute the previous query from the query stacks. -WHENCE determines where the query is taken from and is a symbol, -either `future' or `past'." - (let ((query (mu4e~headers-pop-query whence)) - (where (if (eq whence 'future) 'past 'future))) - (when query - (mu4e~headers-push-query mu4e~headers-last-query where) - (mu4e-headers-search query nil nil t)))) - -(defun mu4e-headers-query-next () - "Execute the previous query from the query stacks." - (interactive) - (mu4e~headers-query-navigate 'future)) - -(defun mu4e-headers-query-prev () - "Execute the previous query from the query stacks." - (interactive) - (mu4e~headers-query-navigate 'past)) - -;; forget the past so we don't repeat it :/ -(defun mu4e-headers-forget-queries () - "Forget all the complete query history." - (interactive) - (setq ;; note: don't forget the present one - mu4e~headers-query-past nil - mu4e~headers-query-future nil) - (mu4e-message "Query history cleared")) (defun mu4e~headers-move (lines) "Move point LINES lines forward (if LINES is positive) or @@ -1917,9 +1659,9 @@ given, offer to edit the search query before executing it." (list maildir current-prefix-arg))) (when maildir (let* ((query (format "maildir:\"%s\"" maildir)) - (query (if edit (mu4e-read-query "Refine query: " query) query))) + (query (if edit (mu4e-search-read-query "Refine query: " query) query))) (mu4e-mark-handle-when-leaving) - (mu4e-headers-search query)))) + (mu4e-search query)))) (defun mu4e-headers-split-view-grow (&optional n) "In split-view, grow the headers window. diff --git a/mu4e/mu4e-main.el b/mu4e/mu4e-main.el index 6e01ceb8..5a1f630b 100644 --- a/mu4e/mu4e-main.el +++ b/mu4e/mu4e-main.el @@ -27,7 +27,9 @@ (require 'smtpmail) ;; the queueing stuff (silence elint) (require 'mu4e-utils) ;; utility functions (require 'mu4e-context) ;; the context +(require 'mu4e-search) (require 'mu4e-vars) ;; mu-wide variables + (require 'cl-lib) ;;; Mode @@ -50,10 +52,6 @@ no unread messages.") (defvar mu4e-main-mode-map (let ((map (make-sparse-keymap))) - (define-key map "b" 'mu4e-headers-search-bookmark) - (define-key map "B" 'mu4e-headers-search-bookmark-edit) - - (define-key map "s" 'mu4e-headers-search) (define-key map "q" 'mu4e-quit) (define-key map "j" 'mu4e~headers-jump-to-maildir) (define-key map "C" 'mu4e-compose-new) @@ -86,6 +84,7 @@ no unread messages.") (setq truncate-lines t overwrite-mode 'overwrite-mode-binary) (mu4e-context-minor-mode) + (mu4e-search-minor-mode) (set (make-local-variable 'revert-buffer-function) #'mu4e~main-view-real)) @@ -165,7 +164,7 @@ clicked." "Return a string of maildirs with their counts." (cl-loop with mds = (mu4e~maildirs-with-query) with longest = (mu4e~longest-of-maildirs-and-bookmarks) - with queries = (plist-get mu4e~server-props :queries) + with queries = (plist-get mu4e--server-props :queries) for m in mds for key = (string (plist-get m :key)) for name = (plist-get m :name) @@ -276,7 +275,7 @@ When REFRESH is non nil refresh infos from server." (mu4e~key-val "database-path" (mu4e-database-path)) (mu4e~key-val "maildir" (mu4e-root-maildir)) (mu4e~key-val "in store" - (format "%d" (plist-get mu4e~server-props :doccount)) "messages") + (format "%d" (plist-get mu4e--server-props :doccount)) "messages") (if mu4e-main-hide-personal-addresses "" (mu4e~key-val "personal addresses" (if addrs (mapconcat #'identity addrs ", " ) "none")))) diff --git a/mu4e/mu4e-search.el b/mu4e/mu4e-search.el new file mode 100644 index 00000000..55393123 --- /dev/null +++ b/mu4e/mu4e-search.el @@ -0,0 +1,446 @@ +;;; mu4e-search.el -- part of mu4e -*- lexical-binding: t -*- + +;; Copyright (C) 2021 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema +;; Maintainer: Dirk-Jan C. Binnema + +;; This file is not part of GNU Emacs. + +;; mu4e 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. + +;; mu4e 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 mu4e. If not, see . + +;;; Commentary: + +;; Search-related functions and a minor-mode. + +;;; Code: + +(require 'seq) +(require 'cl-lib) +(require 'mu4e-helpers) + + +;;; Configuration +(defgroup mu4e-search nil + "Search-related settings." + :group 'mu4e) + +(defcustom mu4e-search-results-limit 500 + "Maximum number of results to show. +This affects performance, especially when +`mu4e-summary-include-related' is non-nil. +Set to -1 for no limits." + :type '(choice (const :tag "Unlimited" -1) + (integer :tag "Limit")) + :group 'mu4e-search) +(define-obsolete-variable-alias 'mu4e-headers-results-limit + 'mu4e-search-results-limit "1.7.0") + +(defvar mu4e-search-full nil + "Whether to search for all results. +If this is nil, search for up to `mu4e-search-results-limit')") + +(define-obsolete-variable-alias 'mu4e-headers-full-search + 'mu4e-search-full "1.7.0") + +(defvar mu4e-search-threads t + "Whether to calculate threads for the search results.") +(define-obsolete-variable-alias 'mu4e-headers-show-threads + 'mu4e-search-threads "1.7.0") + +(defcustom mu4e-search-query-rewrite-function 'identity + "Function to rewrite a query. + +It takes a search expression string, and returns a possibly + changed search expression string. + +This function is applied on the search expression just before +searching, and allows users to modify the query. + +For instance, we could change and of workmail into +\"maildir:/long-path-to-work-related-emails\", by setting the function + +(setq mu4e-query-rewrite-function + (lambda(expr) + (replace-regexp-in-string \"workmail\" + \"maildir:/long-path-to-work-related-emails\" expr))) + +It is good to remember that the replacement does not understand +anything about the query, it just does text replacement." + :type 'function + :group 'mu4e-search) + +(define-obsolete-variable-alias 'mu4e-query-rewrite-function + 'mu4e-search-query-rewrite-function "1.7.0") + +(defcustom mu4e-search-bookmark-hook nil + "Hook run just after invoking a bookmarked search. + +This function receives the query as its parameter, before any +rewriting as per `mu4e-query-rewrite-function' has taken place. + +The reason to use this instead of `mu4e-headers-search-hook' is +if you only want to execute a hook when a search is entered via a +bookmark, e.g. if you'd like to treat the bookmarks as a custom +folder and change the options for the search." + :type 'hook + :group 'mu4e-search) + +(define-obsolete-variable-alias + 'mu4e-headers-search-bookmark-hook + 'mu4e-search-bookmark-hook "1.7.0") + +(defcustom mu4e-search-hook nil + "Hook run just before executing a new search operation. +This function receives the query as its parameter, before any +rewriting as per `mu4e-query-rewrite-function' has taken place + +This is a more general hook facility than the +`mu4e-search-bookmark-hook'. It gets called on every +executed search, not just those that are invoked via bookmarks, +but also manually invoked searches." + :type 'hook + :group 'mu4e-search) + +(define-obsolete-variable-alias 'mu4e-headers-search-hook + 'mu4e-search-hook "1.7.0") + +;;; Interactive functions + +(defun mu4e-search (&optional expr prompt edit ignore-history msgid show) + "Search for query EXPR. + +Switch to the output buffer for the results. This is an +interactive function which ask user for EXPR. PROMPT, if non-nil, +is the prompt used by this function (default is \"Search for:\"). +If EDIT is non-nil, instead of executing the query for EXPR, let +the user edit the query before executing it. + +If IGNORE-HISTORY is true, do *not* update the query history +stack. If MSGID is non-nil, attempt to move point to the first +message with that message-id after searching. If SHOW is non-nil, +show the message with MSGID." + (interactive) + (let* ((prompt (mu4e-format (or prompt "Search for: "))) + (expr + (if (or (null expr) edit) + (mu4e-read-query prompt expr) + expr))) + (mu4e-mark-handle-when-leaving) + (mu4e--search-execute expr ignore-history) + (setq mu4e~headers-msgid-target msgid + mu4e~headers-view-target show))) + +(define-obsolete-function-alias 'mu4e-headers-search 'mu4e-search "1.7.0") + +(defun mu4e-search-edit () + "Edit the last search expression." + (interactive) + (mu4e-search mu4e--search-last-query nil t)) + +(define-obsolete-variable-alias 'mu4e-headers-search-edit + 'mu4e-search-edit "1.7.0") + +(defun mu4e-search-bookmark (&optional expr edit) + "Search using some bookmarked query EXPR. +If EDIT is non-nil, let the user edit the bookmark before starting +the search." + (interactive) + (let ((expr + (or expr + (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: "))))) + (run-hook-with-args 'mu4e-search-bookmark-hook expr) + (mu4e-search expr (when edit "Edit bookmark: ") edit))) + +(define-obsolete-function-alias 'mu4e-headers-search-bookmark + 'mu4e-search-bookmark "1.7.0") + +(defun mu4e-search-bookmark-edit () + "Edit an existing bookmark before executing it." + (interactive) + (mu4e-search-bookmark nil t)) + +(define-obsolete-function-alias 'mu4e-headers-search-bookmark-edit + 'mu4e-search-bookmark-edit "1.7.0") + +(defun mu4e-search-narrow(&optional filter) + "Narrow the last search. +Do so by appending search expression FILTER to the last search +expression. Note that you can go back to previous +query (effectively, 'widen' it), with `mu4e-search-prev'." + (interactive + (let ((filter + (read-string (mu4e-format "Narrow down to: ") + nil 'mu4e~headers-search-hist nil t))) + (list filter))) + (unless mu4e--search-last-query + (mu4e-warn "There's nothing to filter")) + (mu4e-headers-search + (format "(%s) AND (%s)" mu4e--search-last-query filter))) + +(define-obsolete-function-alias 'mu4e-headers-search-narrow + 'mu4e-search-narrow "1.7.0") + +;; (defun mu4e-headers-change-sorting (&optional field dir) +;; "Change the sorting/threading parameters. +;; FIELD is the field to sort by; DIR is a symbol: either 'ascending, +;; 'descending, 't (meaning: if FIELD is the same as the current +;; sortfield, change the sort-order) or nil (ask the user)." +;; (interactive) +;; (let* ((field +;; (or field +;; (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices))) +;; ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is +;; ;; sortable field), _or_ another field (meaning: sort by this other field). +;; (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable)) +;; ;; error check +;; (sortable +;; (if sortable +;; sortable +;; (mu4e-error "Not a sortable field"))) +;; (sortfield (if (booleanp sortable) field sortable)) +;; (dir +;; (cl-case dir +;; ((ascending descending) dir) +;; ;; change the sort order if field = curfield +;; (t +;; (if (eq sortfield mu4e-headers-sort-field) +;; (if (eq mu4e-headers-sort-direction 'ascending) +;; 'descending 'ascending) +;; 'descending)) +;; (mu4e-read-option "Direction: " +;; '(("ascending" . 'ascending) ("descending" . 'descending)))))) +;; (setq +;; mu4e-headers-sort-field sortfield +;; mu4e-headers-sort-direction dir) +;; (mu4e-message "Sorting by %s (%s)" +;; (symbol-name sortfield) +;; (symbol-name mu4e-headers-sort-direction)) +;; (mu4e-headers-rerun-search))) + +;; (defun mu4e~headers-toggle (name togglevar dont-refresh) +;; "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil, +;; re-run the last search." +;; (set togglevar (not (symbol-value togglevar))) +;; (mu4e-message "%s turned %s%s" +;; name +;; (if (symbol-value togglevar) "on" "off") +;; (if dont-refresh +;; " (press 'g' to refresh)" "")) +;; (unless dont-refresh +;; (mu4e-headers-rerun-search))) + +;; (defun mu4e-headers-toggle-threading (&optional dont-refresh) +;; "Toggle `mu4e-headers-show-threads'. With prefix-argument, do +;; _not_ refresh the last search with the new setting for threading." +;; (interactive "P") +;; (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh)) + +;; (defun mu4e-headers-toggle-full-search (&optional dont-refresh) +;; "Toggle `mu4e-headers-full-search'. With prefix-argument, do +;; _not_ refresh the last search with the new setting for threading." +;; (interactive "P") +;; (mu4e~headers-toggle "Full-search" +;; 'mu4e-headers-full-search dont-refresh)) + +;; (defun mu4e-headers-toggle-include-related (&optional dont-refresh) +;; "Toggle `mu4e-headers-include-related'. With prefix-argument, do +;; _not_ refresh the last search with the new setting for threading." +;; (interactive "P") +;; (mu4e~headers-toggle "Include-related" +;; 'mu4e-headers-include-related dont-refresh)) + +;; (defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh) +;; "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do +;; _not_ refresh the last search with the new setting for threading." +;; (interactive "P") +;; (mu4e~headers-toggle "Skip-duplicates" +;; 'mu4e-headers-skip-duplicates dont-refresh)) + + +;;; History + +(defvar mu4e--search-last-query nil + "The present (most recent) query.") +(defvar mu4e--search-query-past nil + "Stack of queries before the present one.") +(defvar mu4e--search-query-future nil + "Stack of queries after the present one.") +(defvar mu4e--search-query-stack-size 20 + "Maximum size for the query stacks.") + +(defun mu4e--search-push-query (query where) + "Push QUERY to one of the query stacks. +WHERE is a symbol telling us where to push; it's a symbol, either +'future or 'past. Functional also removes duplicates, limits the +stack size." + (let ((stack + (cl-case where + (past mu4e--search-query-past) + (future mu4e--search-query-future)))) + ;; only add if not the same item + (unless (and stack (string= (car stack) query)) + (push query stack) + ;; limit the stack to `mu4e--search-query-stack-size' elements + (when (> (length stack) mu4e--search-query-stack-size) + (setq stack (cl-subseq stack 0 mu4e--search-query-stack-size))) + ;; remove all duplicates of the new element + (cl-remove-if (lambda (elm) (string= elm (car stack))) (cdr stack)) + ;; update the stacks + (cl-case where + (past (setq mu4e--search-query-past stack)) + (future (setq mu4e--search-query-future stack)))))) + +(defun mu4e--search-pop-query (whence) + "Pop a query from the stack. +WHENCE is a symbol telling us where to get it from, either `future' +or `past'." + (cl-case whence + (past + (unless mu4e--search-query-past + (mu4e-warn "No more previous queries")) + (pop mu4e--search-query-past)) + (future + (unless mu4e--search-query-future + (mu4e-warn "No more next queries")) + (pop mu4e--search-query-future)))) + + +(defun mu4e-search-rerun () + "Re-run the search for the last search expression." + (interactive) + ;; if possible, try to return to the same message + (let* ((msg (mu4e-message-at-point t)) + (msgid (and msg (mu4e-message-field msg :message-id)))) + (mu4e-headers-search mu4e~headers-last-query nil nil t msgid))) + +(define-obsolete-function-alias 'mu4e-headers-rerun-search + 'mu4e-search-rerun "1.7.0") + +(defun mu4e--search-query-navigate (whence) + "Execute the previous query from the query stacks. +WHENCE determines where the query is taken from and is a symbol, +either `future' or `past'." + (let ((query (mu4e--search-pop-query whence)) + (where (if (eq whence 'future) 'past 'future))) + (when query + (mu4e--search-push-query mu4e--search-last-query where) + (mu4e-search query nil nil t)))) + +(defun mu4e-search-next () + "Execute the next query from the query stack." + (interactive) + (mu4e--search-query-navigate 'future)) + +(define-obsolete-function-alias 'mu4e-headers-query-next + 'mu4e-search-next "1.7.0") + +(defun mu4e-search-prev () + "Execute the previous query from the query stacks." + (interactive) + (mu4e--search-query-navigate 'past)) + +(define-obsolete-function-alias 'mu4e-headers-query-prev + 'mu4e-search-prev "1.7.0") + +;; forget the past so we don't repeat it :/ +(defun mu4e-search-forget () + "Forget the search history." + (interactive) + (setq mu4e--search-query-past nil + mu4e--search-query-future nil) + (mu4e-message "Query history cleared")) + +(define-obsolete-function-alias 'mu4e-headers-forget-queries + 'mu4e-search-forget "1.7.0") + +(defun mu4e-last-query () + "Get the most recent query or nil if there is none." + mu4e--search-last-query) + +;;; Completion for queries + +(defvar mu4e--search-hist nil "History list of searches.") +(defvar mu4e-minibuffer-search-query-map + (let ((map (copy-keymap minibuffer-local-map))) + (define-key map (kbd "TAB") #'completion-at-point) + map) + "The keymap for reading a search query.") + +(defun mu4e-search-read-query (prompt &optional initial-input) + "Read a query with completion using PROMPT and INITIAL-INPUT." + (minibuffer-with-setup-hook + (lambda () + (setq-local completion-at-point-functions + #'mu4e--search-query-competion-at-point) + (use-local-map mu4e-minibuffer-search-query-map)) + (read-string prompt initial-input 'mu4e--search-hist))) + +(define-obsolete-function-alias 'mu4e-read-query + 'mu4e-search-read-query "1.7.0") + +(defconst mu4e--search-query-keywords + '("and" "or" "not" + "from:" "to:" "cc:" "bcc:" "contact:" "date:" "subject:" "body:" + "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:" + "size:" "embed:")) + +(defun mu4e--search-query-competion-at-point () + "Provide completion when entering search expressions." + (cond + ((not (looking-back "[:\"][^ \t]*" nil)) + (let ((bounds (bounds-of-thing-at-point 'word))) + (list (or (car bounds) (point)) + (or (cdr bounds) (point)) + mu4e--search-query-keywords))) + ((looking-back "flag:\\(\\w*\\)" nil) + (list (match-beginning 1) + (match-end 1) + '("attach" "draft" "flagged" "list" "new" "passed" "replied" + "seen" "trashed" "unread" "encrypted" "signed"))) + ;; ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil) + ;; (list (match-beginning 1) + ;; (match-end 1) + ;; (mu4e-get-maildirs))) + ((looking-back "prio:\\(\\w*\\)" nil) + (list (match-beginning 1) + (match-end 1) + (list "high" "normal" "low"))) + ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil) + (list (match-beginning 1) + (match-end 1) + (mailcap-mime-types))))) + +(define-minor-mode mu4e-search-minor-mode + "Mode for searching for messages." + :global nil + :init-value nil ;; disabled by default + :group 'mu4e + :lighter "" + :keymap + (let ((map (make-sparse-keymap))) + (define-key map "s" 'mu4e-search) + (define-key map "S" 'mu4e-search-edit) + (define-key map "/" 'mu4e-search-narrow) + ;;(define-key map "j" 'mu4e~headers-jump-to-maildir) + (define-key map (kbd "") 'mu4e-search-prev) + (define-key map (kbd "\\") 'mu4e-search-prev) + (define-key map (kbd "") 'mu4e-search-next) + + (define-key map "b" 'mu4e-search-bookmark) + (define-key map "B" 'mu4e-search-bookmark-edit) + map)) + +(provide 'mu4e-search) +;;; mu4e-search.el ends here diff --git a/mu4e/mu4e-view-gnus.el b/mu4e/mu4e-view-gnus.el index 2b73f649..ad5af171 100644 --- a/mu4e/mu4e-view-gnus.el +++ b/mu4e/mu4e-view-gnus.el @@ -29,6 +29,7 @@ (require 'mu4e-view-common) (require 'mu4e-context) +(require 'mu4e-search) (require 'calendar) (require 'gnus-art) @@ -237,16 +238,6 @@ This is useful for advising some Gnus-functionality that does not work in mu4e." ;; but that's not very useful in this case (define-key map "z" 'ignore) - (define-key map "s" #'mu4e-headers-search) - (define-key map "S" #'mu4e-view-search-edit) - (define-key map "/" #'mu4e-view-search-narrow) - - (define-key map (kbd "") #'mu4e-headers-query-prev) - (define-key map (kbd "") #'mu4e-headers-query-next) - - (define-key map "b" #'mu4e-headers-search-bookmark) - (define-key map "B" #'mu4e-headers-search-bookmark-edit) - (define-key map "%" #'mu4e-view-mark-pattern) (define-key map "t" #'mu4e-view-mark-subthread) (define-key map "T" #'mu4e-view-mark-thread) @@ -320,7 +311,6 @@ This is useful for advising some Gnus-functionality that does not work in mu4e." (define-key map (kbd "") #'mu4e-view-mark-for-something) (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks) - ;; misc (define-key map "M" #'mu4e-view-massage) @@ -421,6 +411,7 @@ Based on Gnus' article-mode." "." (apply func args)))) (use-local-map mu4e-view-mode-map) (mu4e-context-minor-mode) + (mu4e-search-minor-mode) (setq buffer-undo-list t);; don't record undo info ;; autopair mode gives error when pressing RET ;; turn it off