Improve completion-at-point' for org-contacts.el' in mail

* org-contacts.el: Improve the completion part:
- When a group is found, it now replaces the name of the group by the
  addresses of the member of the group rather than appending the
  addresses.
- One can now complete on all part of an address and not only on the
  beginning of the name.
This commit is contained in:
Grégoire Jadi 2013-01-28 11:24:39 +01:00 committed by Bastien Guerry
parent 93f982e3ba
commit 50d8ec9752

View file

@ -179,27 +179,215 @@ If both match values are nil, return all contacts."
(let ((completion-ignore-case (not dont-fold))) (let ((completion-ignore-case (not dont-fold)))
(complete-with-action action table string pred))))) (complete-with-action action table string pred)))))
(defun org-contacts-complete-name (&optional start) (defun org-contacts-try-completion-prefix (to-match collection &optional predicate)
"Complete text at START with a user name and email." "Like `try-completion' but:
(let* ((end (point)) - works only with list and alist;
(start (or start - looks at all prefixes rather than just the beginning of the string;"
(save-excursion (loop with regexp = (concat "\\b" (regexp-quote to-match))
(re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*") with ret = nil
(goto-char (match-end 0)) with ret-start = nil
(point)))) with ret-end = nil
(orig (buffer-substring start end))
(completion-ignore-case org-contacts-completion-ignore-case) for el in collection
for string = (if (listp el) (car el) el)
for start = (when (or (null predicate) (funcall predicate string))
(string-match regexp string))
if start
do (let ((end (match-end 0))
(len (length string)))
(if (= end len)
(return t)
(destructuring-bind (string start end)
(if (null ret)
(values string start end)
(org-contacts-common-substring
ret ret-start ret-end
string start end))
(setf ret string
ret-start start
ret-end end))))
finally (return
(replace-regexp-in-string "\\`[ \t\n]*" "" ret))))
(defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2 &optional ignore-case)
"Compare the contents of two strings, using `compare-strings'.
This function works like `compare-strings' excepted that it
returns a cons.
- The CAR is the number of characters that match at the beginning.
- The CDR is T is the two strings are the same and NIL otherwise."
(let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case)))
(if (eq ret t)
(cons (or end1 (length s1)) t)
(cons (1- (abs ret)) nil))))
(defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2)
"Extract the common substring between S1 and S2.
This function extracts the common substring between S1 and S2 and
adjust the part that remains common.
START1 and END1 delimit the part in S1 that we know is common
between the two strings. This applies to START2 and END2 for S2.
This function returns a list whose contains:
- The common substring found.
- The new value of the start of the known inner substring.
- The new value of the end of the known inner substring."
;; Given two strings:
;; s1: "foo bar baz"
;; s2: "fooo bar baz"
;; and the inner substring is "bar"
;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
;;
;; To find the common substring we will compare two substrings:
;; " oof" and " ooof" to find the beginning of the common substring.
;; " baz" and " baz" to find the end of the common substring.
(let* ((len1 (length s1))
(start1 (or start1 0))
(end1 (or end1 len1))
(len2 (length s2))
(start2 (or start2 0))
(end2 (or end2 len2))
(new-start (car (org-contacts-compare-strings
(substring (org-reverse-string s1) (- len1 start1)) nil nil
(substring (org-reverse-string s2) (- len2 start2)) nil nil)))
(new-end (+ end1 (car (org-contacts-compare-strings
(substring s1 end1) nil nil
(substring s2 end2) nil nil)))))
(list (substring s1 (- start1 new-start) new-end)
new-start
(+ new-start (- end1 start1)))))
(defun org-contacts-all-completions-prefix (to-match collection &optional predicate)
"Like `all-completions' but:
- works only with list and alist;
- looks at all prefixes rather than just the beginning of the string;"
(loop with regexp = (concat "\\b" (regexp-quote to-match))
for el in collection
for string = (if (listp el) (car el) el)
for match? = (when (and (or (null predicate) (funcall predicate string)))
(string-match regexp string))
if match?
collect (progn
(let ((end (match-end 0)))
(org-no-properties string)
(when (< end (length string))
;; Here we add a text property that will be used
;; later to highlight the character right after
;; the common part between each addresses.
;; See `org-contacts-display-sort-function'.
(put-text-property end (1+ end) 'org-contacts-prefix 't string)))
string)))
(defun org-contacts-make-collection-prefix (collection)
"Makes a collection function from COLLECTION which will match
on prefixes."
(lexical-let ((collection collection))
(lambda (string predicate flag)
(cond ((eq flag nil)
(org-contacts-try-completion-prefix string collection predicate))
((eq flag t)
;; `org-contacts-all-completions-prefix' has already been
;; used to compute `all-completions'.
collection)
((eq flag 'lambda)
(org-contacts-test-completion-prefix string collection predicate))
((and (listp flag) (eq (car flag) 'boundaries))
(destructuring-bind (to-ignore &rest suffix)
flag
(org-contacts-boundaries-prefix string collection predicate suffix)))
((eq flag 'metadata)
(org-contacts-metadata-prefix string collection predicate))
(t nil ; operation unsupported
)))))
(defun org-contacts-display-sort-function (completions)
(mapcar (lambda (string)
(loop with len = (1- (length string))
for i upfrom 0 to len
if (memq 'org-contacts-prefix
(text-properties-at i string))
do (set-text-properties
i (1+ i)
(list 'font-lock-face
(if (char-equal (aref string i)
(string-to-char " "))
;; Spaces can't be bold.
'underline
'bold)) string)
else
do (set-text-properties i (1+ i) nil string)
finally (return string)))
completions))
(defun org-contacts-test-completion-prefix (string collection predicate)
(find-if (lambda (el)
(and (or (null predicate) (funcall predicate el))
(string= string el)))
collection))
(defun org-contacts-boundaries-prefix (string collection predicate suffix)
(list* 'boundaries (completion-boundaries string collection predicate suffix)))
(defun org-contacts-metadata-prefix (string collection predicate)
'(metadata .
((display-sort-function . org-contacts-display-sort-function))))
(defun org-contacts-complete-group (start end string)
"Complete text at START from a group.
A group FOO is composed of contacts with the tag FOO."
(let* ((completion-ignore-case org-contacts-completion-ignore-case)
(group-completion-p (org-string-match-p (group-completion-p (org-string-match-p
(concat "^" org-contacts-group-prefix) orig)) (concat "^" org-contacts-group-prefix) string)))
(completion-list (when group-completion-p
(if group-completion-p (let ((completion-list
(mapcar (lambda (group) (propertize (concat org-contacts-group-prefix group) (all-completions
string
(mapcar (lambda (group)
(propertize (concat org-contacts-group-prefix group)
'org-contacts-group group)) 'org-contacts-group group))
(org-uniquify (org-uniquify
(loop for contact in (org-contacts-filter) (loop for contact in (org-contacts-filter)
with group-list
nconc (org-split-string nconc (org-split-string
(or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))) (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))))))
(list start end
(if (= (length completion-list) 1)
;; We've foudn the correct group, returns the address
(lexical-let ((tag (get-text-property 0 'org-contacts-group
(car completion-list))))
(lambda (string pred &optional to-ignore)
(mapconcat 'identity
(loop for contact in (org-contacts-filter
nil
tag)
;; The contact name is always the car of the assoc-list
;; returned by `org-contacts-filter'.
for contact-name = (car contact)
;; Grab the first email of the contact
for email = (car (split-string
(or
(cdr (assoc-string org-contacts-email-property
(caddr contact)))
"")))
;; If the user has an email address, append USER <EMAIL>.
if email collect (org-contacts-format-email contact-name email))
", ")))
;; We haven't found the correct group
(completion-table-case-fold completion-list
(not org-contacts-completion-ignore-case))))))))
(defun org-contacts-complete-name (start end string)
"Complete text at START with a user name and email."
(let* ((completion-ignore-case org-contacts-completion-ignore-case)
(completion-list
(loop for contact in (org-contacts-filter) (loop for contact in (org-contacts-filter)
;; The contact name is always the car of the assoc-list ;; The contact name is always the car of the assoc-list
;; returned by `org-contacts-filter'. ;; returned by `org-contacts-filter'.
@ -213,41 +401,28 @@ If both match values are nil, return all contacts."
;; … append a list of USER <EMAIL>. ;; … append a list of USER <EMAIL>.
nconc (loop for email in email-list nconc (loop for email in email-list
collect (org-contacts-format-email contact-name email))))) collect (org-contacts-format-email contact-name email)))))
(completion-list (all-completions orig completion-list))) (when completion-list
;; If we are completing a group, and that's the only group, just return
;; the real result.
(when (and group-completion-p
(= (length completion-list) 1))
(setq completion-list
(list (concat
(car completion-list) ";: "
(mapconcat 'identity
(loop for contact in (org-contacts-filter
nil
(get-text-property 0 'org-contacts-group
(car completion-list)))
;; The contact name is always the car of the assoc-list
;; returned by `org-contacts-filter'.
for contact-name = (car contact)
;; Grab the first email of the contact
for email = (car (split-string
(or
(cdr (assoc-string org-contacts-email-property
(caddr contact)))
"")))
;; If the user has an email address, append USER <EMAIL>.
if email collect (org-contacts-format-email contact-name email))
", ")))))
(list start end (list start end
(completion-table-case-fold completion-list (org-contacts-make-collection-prefix
(not org-contacts-completion-ignore-case))))) (org-contacts-all-completions-prefix
string
(remove-duplicates completion-list :test #'equalp)))))))
(defun org-contacts-message-complete-function () (defun org-contacts-message-complete-function (&optional start)
"Function used in `completion-at-point-functions' in `message-mode'." "Function used in `completion-at-point-functions' in `message-mode'."
(let ((mail-abbrev-mode-regexp (let ((mail-abbrev-mode-regexp
"^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):")) "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
(when (mail-abbrev-in-expansion-header-p) (when (mail-abbrev-in-expansion-header-p)
(org-contacts-complete-name)))) (lexical-let*
((end (point))
(start (or start
(save-excursion
(re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
(goto-char (match-end 0))
(point))))
(string (buffer-substring start end)))
(or (org-contacts-complete-group start end string)
(org-contacts-complete-name start end string))))))
(defun org-contacts-gnus-get-name-email () (defun org-contacts-gnus-get-name-email ()
"Get name and email address from Gnus message." "Get name and email address from Gnus message."